package com.jscape.inet.http;


import java.io.DataInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Vector;


/**
 * An InputStream for reading HTTP/1.1 chunked streams.
 */

class ChunkedInputStream
        extends FilterInputStream {

    private DataInputStream din;
    private int contentLength;
    private static final String CRLF = new String("\r\n");

    /// Make a ChunkedInputStream.
    ChunkedInputStream(InputStream in) {
        super(new DataInputStream(in));
        din = (DataInputStream) this.in;
        contentLength = 0;
    }

    private byte[] b1 = new byte[1];

    /// The FilterInputStream implementation of the single-byte read()
    // method just reads directly from the underlying stream.  We want
    // to go through our own read-block method, so we have to override.
    // Seems like FilterInputStream really ought to do this itself.
    public int read()
            throws IOException {
        if (read(b1, 0, 1) == -1) {
            return -1;
        } else {
            return b1[0];
        }
    }

    private int chunkCount = 0;

    /// Reads into an array of bytes.
    // @param b the buffer into which the data is read
    // @param off the start offset of the data
    // @param len the maximum number of bytes read
    // @return the actual number of bytes read, or -1 on EOF
    // @exception IOException if an I/O error has occurred
    public int read(byte[] b, int off, int len)
            throws IOException {
        if (chunkCount == 0) {
            startChunk();
            if (chunkCount == 0) {
                return -1;
            }
        }
        int toRead = Math.min(chunkCount, len);
        int r = din.read(b, off, toRead);

        if (r != -1) {
            chunkCount -= r;
        }
        return r;
    }

    /**
     * Reads the start of a chunk.
     *
     * @throws IOException if an I/O related error occurs
     */
    private void startChunk()
            throws IOException {
        String line = readLine();

        // eliminate possible leading empty lines
        while (line.length() == 0) {
            line = readLine();
        }
        line = line.trim();
        try {
            chunkCount = Integer.parseInt(line, 16);
        } catch (NumberFormatException e) {
            throw new IOException("malformed chunk");
        }
        contentLength += chunkCount;
        if (chunkCount == 0) {
            readFooters();
        }
    }

    private Vector footerNames = null;
    private Vector footerValues = null;

    /// Reads footers.
    @SuppressWarnings("unchecked")
    private void readFooters()
            throws IOException {
        footerNames = new Vector();
        footerValues = new Vector();
        String line;

        while (true) {
            line = readLine();
            if (line.length() == 0) {
                break;
            }
            int colon = line.indexOf(':');

            if (colon != -1) {
                String name = line.substring(0, colon).toLowerCase(Locale.ENGLISH).trim();
                String value = line.substring(colon + 1).trim();

                footerNames.addElement(name.toLowerCase(Locale.ENGLISH));
                footerValues.addElement(value);
            }
        }
    }

    /**
     * Returns the value of a footer field, or null if not known.
     * Footers come at the end of a chunked stream, so trying to
     * retrieve them before the stream has given an EOF will return
     * only nulls.
     *
     * @param name the footer field name
     *
     * @return the value of footer
     */
    public String getFooter(String name) {
        if (!isDone()) {
            return null;
        }
        int i = footerNames.indexOf(name.toLowerCase(Locale.ENGLISH));

        if (i == -1) {
            return null;
        }
        return (String) footerValues.elementAt(i);
    }


    /**
     * @return all footers
     */
    public Enumeration getFooters() {
        if (!isDone()) {
            return null;
        }
        return footerNames.elements();
    }

    /// Returns the size of the request entity data, or -1 if not known.

    /**
     * Gets content length of input stream
     *
     * @return the length of response in bytes
     */
    public int getContentLength() {
        if (!isDone()) {
            return -1;
        }
        return contentLength;
    }

    /**
     * Tells whether the stream has gotten to its end yet.
     *
     * @return <CODE>true</CODE> if finished, <CODE>false</CODE> otherwise
     */
    public boolean isDone() {
        return footerNames != null;
    }

    /**
     * Read CRLF retminated line from server.
     *
     * @return the next line read
     *
     * @throws IOException if an I/O related error occurs
     */
    private String readLine()
            throws IOException {
        StringBuffer line = new StringBuffer();
        int matched = 0;

        while (matched != CRLF.length()) {
            int rd = din.read();

            if (rd == -1) {
                break;
            }
            char a = (char) rd;

            if (a == CRLF.charAt(matched)) {
                matched++;
                continue;
            }
            if (matched > 0) {
                line.append(CRLF.substring(0, matched));
                matched = 0;
            }
            line.append(a);
        }
        return line.toString();
    }

} 

