/*
 * 
 * Copyright 2004 Sun Microsystems, Inc. All Rights Reserved.
 * 
 * The contents of this file are subject to the Sun Community Source
 * License v3.0/Jini Technology Specific Attachment v 1.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://wwws.sun.com/software/jini/licensing/SCSL3_JiniTSA1.html.
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
 * the License for the specific language governing rights and limitations
 * under the License.
 * 
 * The Reference Code is Jini Technology Core Platform code, v2.0. The
 * Developer of the Reference Code is Sun Microsystems, Inc.
 * 
 * Contributor(s): Sun Microsystems, Inc.
 * 
 * The contents of this file comply with the Jini Technology Core
 * Platform Compatibility Kit, v2.0A.
 * 
 * Tester(s): Sun Microsystems, Inc.
 * 
 * Test Platform(s):
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Solaris(TM)SPARC/x86
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Linux (Intel x86)
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Windows (Intel Platform)
 * 
 */

package com.sun.jini.jeri.internal.http;

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.util.StringTokenizer;

/**
 * Class for reading HTTP messages.  Each instance reads a single HTTP message.
 *
 * @author Sun Microsystems, Inc.
 * 
 */
class MessageReader {
    
    /* state values */
    private static final int START   = 0;
    private static final int HEADER  = 1;
    private static final int CONTENT = 2;
    private static final int DONE    = 3;
    
    private static final byte[] sink = new byte[256];

    private final InputStream in;
    private final boolean noContent;
    private int state = START;
    private StartLine sline;
    private InputStream cin;

    /**
     * Creates new reader on top of given input stream.  If noContent is true,
     * incoming message is assumed to be bodiless (e.g., a HEAD response).
     */
    MessageReader(InputStream in, boolean noContent) {
	this.in = in;
	this.noContent = noContent;
    }
    
    /**
     * Reads in HTTP message start line.
     */
    StartLine readStartLine() throws IOException {
	updateState(START, HEADER);
	sline = new StartLine(in);
	return sline;
    }
    
    /**
     * Reads in HTTP message header.
     */
    Header readHeader() throws IOException {
	updateState(HEADER, CONTENT);
	Header header = new Header(in);

	if (!noContent && contentIndicated(sline, header)) {
	    String clen;
	    if (header.containsValue("Transfer-Encoding", "chunked", true)) {
		cin = new ChunkedInputStream();
	    } else if ((clen = header.getField("Content-Length")) != null) {
		int len;
		try {
		    len = Integer.parseInt(clen);
		} catch (Exception ex) {
		    throw new HttpParseException("invalid content length");
		}
		if (len < 0) {
		    throw new HttpParseException("invalid content length");
		}
		cin = new BoundedInputStream(len);
	    } else if (sline.isRequest) {
		throw new HttpParseException("request length undeclared");
	    } else {
		cin = in;
	    }
	} else {
	    cin = new BoundedInputStream(0);
	}

	return header;
    }
    
    /**
     * Reads message content.
     */
    int readContent(byte[] b, int off, int len) throws IOException {
	updateState(CONTENT, CONTENT);
	return cin.read(b, off, len);
    }
    
    /**
     * Returns count of available message content.
     */
    int availableContent() throws IOException {
	updateState(CONTENT, CONTENT);
	return cin.available();
    }
    
    /**
     * Reads in message trailer after consuming any unread content data.
     * Returns null if message doesn't have a trailer.
     */
    Header readTrailer() throws IOException {
	updateState(CONTENT, CONTENT);
	while (cin.read(sink) != -1) {
	}

	updateState(CONTENT, DONE);
	if (cin instanceof ChunkedInputStream) {
	    Header trailer = new Header(in);
	    return (trailer.size() > 0) ? trailer : null;
	} else {
	    return null;
	}
    }

    /**
     * Reads and returns next line from stream, or null if at end of stream.
     * Expects ASCII lines terminated by the HTTP end-of-line sequence "\r\n".
     */
    static String readLine(InputStream in) throws IOException {
	// REMIND: use Charset?
	int c = in.read();
	if (c == -1) {
	    return null;
	}
	StringBuffer sbuf = new StringBuffer();
	do {
	    if (c == '\r') {
		if ((c = in.read()) == '\n') {
		    break;
		}
		sbuf.append('\r');
	    } else {
		sbuf.append((char) c);
		c = in.read();
	    }
	} while (c != -1);
	return sbuf.toString();
    }

    private void updateState(int oldState, int newState) {
	if (state != oldState) {
	    throw new IllegalStateException();
	}
	state = newState;
    }
    
    /**
     * Returns true if given start line and header indicate a content body.
     */
    private static boolean contentIndicated(StartLine sline, Header header) {
	if (!sline.isRequest && 
	    ((sline.status / 100) == 1 ||
	     sline.status == HttpURLConnection.HTTP_NO_CONTENT ||
	     sline.status == HttpURLConnection.HTTP_NOT_MODIFIED))
	{
	    return false;
	}
	
	if (header.getField("Transfer-Encoding") != null ||
	    header.getField("Content-Length") != null)
	{
	    return true;
	}

	return !sline.isRequest;
    }
    
    /**
     * Input stream for reading bounded content data.
     */
    private class BoundedInputStream extends InputStream {
	
	private int bound;
	
	BoundedInputStream(int bound) {
	    this.bound = bound;
	}
	
	public int read() throws IOException {
	    byte[] b = new byte[1];
	    return (read(b, 0, 1) != -1) ? b[0] & 0xFF : -1;
	}
	
	public int read(byte[] b, int off, int len) throws IOException {
	    if (bound == 0) {
		return -1;
	    } else {
		int n = in.read(b, off, Math.min(bound, len));
		if (n != -1) {
		    bound -= n;
		}
		return n;
	    }
	}
	
	public int available() throws IOException {
	    return Math.min(bound, in.available());
	}
    }
    
    /**
     * Input stream for reading chunked content data.
     */
    private class ChunkedInputStream extends InputStream {

	private byte[] buf;
	private int pos = 0;
	private int lim = 0;
	
	ChunkedInputStream() {}

	public int read() throws IOException {
	    byte[] b = new byte[1];
	    return (read(b, 0, 1) != -1) ? b[0] & 0xFF : -1;
	}

	public int read(byte[] b, int off, int len) throws IOException {
	    while (pos >= lim) {
		refill();
	    }
	    if (pos < 0) {
		return -1;
	    }
	    int n = Math.min(lim - pos, len);
	    System.arraycopy(buf, pos, b, off, n);
	    pos += n;
	    return n;
	}
	
	public int available() throws IOException {
	    while (pos >= lim) {
		refill();
	    }
	    return (pos >= 0) ? (lim - pos) : 0;
	}

	private void refill() throws IOException {
	    int newlim = 0;
	    try {
		String line = readLine(in);
		StringTokenizer tok = new StringTokenizer(line, " ;\t");
		newlim = Integer.parseInt(tok.nextToken(), 16);
	    } catch (Exception ex) {
		throw new HttpParseException("error parsing chunk length");
	    }
	    
	    if (newlim < 0) {
		throw new HttpParseException("illegal chunk length");
	    } else if (newlim == 0) {
		pos = -1;
	    } else {
		// REMIND: place upper limit on chunk length?
		if (buf == null || newlim > buf.length) {
		    buf = new byte[newlim];
		}
		for (int i = 0; i < newlim; ) {
		    int n = in.read(buf, i, newlim - i);
		    if (n < 0) {
			throw new EOFException("incomplete chunk");
		    }
		    i += n;
		}
		String blank = readLine(in);
		if (blank == null || blank.length() > 0) {
		    throw new HttpParseException("illegal chunk tail");
		}
		pos = 0;
		lim = newlim;
	    }
	}
    }
}
