/*
 * The contents of this file are subject to the terms
 * of the Common Development and Distribution License
 * (the License).  You may not use this file except in
 * compliance with the License.
 *
 * You can obtain a copy of the license at
 * https://glassfish.dev.java.net/public/CDDLv1.0.html or
 * glassfish/bootstrap/legal/CDDLv1.0.txt.
 * See the License for the specific language governing
 * permissions and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL
 * Header Notice in each file and include the License file
 * at glassfish/bootstrap/legal/CDDLv1.0.txt.
 * If applicable, add the following below the CDDL Header,
 * with the fields enclosed by brackets [] replaced by
 * you own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * Copyright (c) Ericsson AB, 2004-2007. All rights reserved.
 */
package com.ericsson.ssa.utils;

import java.io.ByteArrayOutputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;

import java.util.logging.Level;


/**
 * This class implements an input stream that will
 * parse HTTP chunked encoding. Trailers are discarded!
 *
 *
 *      Chunked-Body   = *chunk
 *                       last-chunk
 *                       trailer
 *                       CRLF
 *      chunk          = chunk-size [ chunk-extension ] CRLF
 *                       chunk-data CRLF
 *      chunk-size     = 1*HEX
 *      last-chunk     = 1*("0") [ chunk-extension ] CRLF
 *      chunk-extension= *( ";" chunk-ext-name [ "=" chunk-ext-val ] )
 *      chunk-ext-name = token
 *      chunk-ext-val  = token | quoted-string
 *      chunk-data     = chunk-size(OCTET)
 *      trailer        = *(entity-header CRLF)
 */
public class ChunkedInputStream extends FilterInputStream {
    /**
     *  The chunk size
     */
    private int chunkSize;

    /**
     * The position within the current chunk
     */
    private int pos;

    /**
     * True if we reached the end of the stream
     */
    private boolean eof = false;

    /**
     * True if we are at the beginning of the stream
     */
    private boolean bof = true;

    /**
     * True if this stream has been closed
     */
    private boolean closed = false;

    /**
     * ChunkedInputStream constructor
     *
     * @param in The chunked input stream
     *
     * @throws IOException If an IO error occured
     */
    public ChunkedInputStream(final InputStream in) throws IOException {
        super(in);

        this.pos = 0;
    }

    /**
     * Reads a byte from the decoded chunked stream.
     *
     * @return The next byte from the chunked stream or -1 if
     *   the end of the stream has been reached.
     * @throws IOException If any IO problem occurs.
     */
    public int read() throws IOException {
        if (closed) {
            throw new IOException("Attempted read from closed stream.");
        }

        if (eof) {
            return -1;
        }

        if (pos >= chunkSize) {
            nextChunk();

            if (eof) {
                return -1;
            }
        }

        pos++;

        return in.read();
    }

    /**
     * Read 'length' bytes from the decoded chunked stream.
     *
     * @param b The byte array that will hold the contents from the stream.
     * @param off The offset into the byte array at which bytes will
     * start to be placed.
     * @param len the maximum number of bytes that can be returned.
     * @return The number of bytes returned or -1 if the end of stream
     * has been reached.
     * @see java.io.InputStream#read(byte[], int, int)
     * @throws IOException if any IO problem occurs.
     */
    public int read(byte[] b, int off, int len) throws IOException {
        if (closed) {
            throw new IOException("Attempted read from closed stream.");
        }

        if (eof) {
            return -1;
        }

        if (pos >= chunkSize) {
            nextChunk();

            if (eof) {
                return -1;
            }
        }

        len = Math.min(len, chunkSize - pos);

        int count = in.read(b, off, len);
        pos += count;

        return count;
    }

    /**
     * Read b.length() bytes from the decoded chunked stream.
     *
     * @param b The byte array that will hold the contents from
     *   the stream.
     * @return The number of bytes returned or -1 if the end
     * of stream has been reached.
     * @see java.io.InputStream#read(byte[])
     * @throws IOException if any IO problem occurs.
     */
    public int read(byte[] b) throws IOException {
        return read(b, 0, b.length);
    }

    /**
     * Read the CRLF terminator.
     *
     * @throws IOException If an IO error occurs.
     */
    private void readCRLF() throws IOException {
        int cr = in.read();
        int lf = in.read();

        if ((cr != '\r') || (lf != '\n')) {
            throw new IOException("CRLF expected at end of chunk: " + cr + "/" +
                lf);
        }
    }

    /**
     * Reads the next chunk.
     *
     * @throws IOException If an IO error occurs.
     */
    private void nextChunk() throws IOException {
        if (!bof) {
            readCRLF();
        }

        chunkSize = getChunkSizeFromInputStream(in);
        bof = false;
        pos = 0;

        if (chunkSize == 0) {
            eof = true;

            //parseTrailerHeaders();
        }
    }

    /**
     * Expects the stream to start with a chunksize in hex with optional
     * comments after a semicolon. The line must end with a CRLF: "a3; some
     * comment\r\n" Positions the stream at the start of the next line.
     *
     * @param in The new input stream.
     * @param required <tt>true<tt/> if a valid chunk must be present,
     *                 <tt>false<tt/> otherwise.
     *
     * @return the chunk size as integer
     *
     * @throws IOException when the chunk size could not be parsed
     */
    private static int getChunkSizeFromInputStream(final InputStream in)
        throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        // States: 0=normal, 1=\r was scanned, 2=inside quoted string, -1=end
        int state = 0;

        while (state != -1) {
            int b = in.read();

            if (b == -1) {
                throw new IOException("chunked stream ended unexpectedly");
            }

            switch (state) {
            case 0:

                switch (b) {
                case '\r':
                    state = 1;

                    break;

                case '\"':
                    state = 2;

                /* fall through */
                default:
                    baos.write(b);
                }

                break;

            case 1:

                if (b == '\n') {
                    state = -1;
                } else {
                    // this was not CRLF
                    throw new IOException("Protocol violation: Unexpected" +
                        " single newline character in chunk size");
                }

                break;

            case 2:

                switch (b) {
                case '\\':
                    b = in.read();
                    baos.write(b);

                    break;

                case '\"':
                    state = 0;

                /* fall through */
                default:
                    baos.write(b);
                }

                break;

            default:
                throw new RuntimeException("assertion failed");
            }
        }

        //parse data
        String dataString = new String(baos.toByteArray(), "US-ASCII");
        int separator = dataString.indexOf(';');
        dataString = (separator > 0)
            ? dataString.substring(0, separator).trim() : dataString.trim();

        int result;

        try {
            result = Integer.parseInt(dataString.trim(), 16);
        } catch (NumberFormatException e) {
            throw new IOException("Bad chunk size: " + dataString);
        }

        return result;
    }

    /**
     * Reads and stores the Trailer headers.
     * @throws IOException If an IO problem occurs
     *
    private void parseTrailerHeaders() throws IOException {
        Header[] footers = null;
        try {
            String charset = "US-ASCII";
            if (this.method != null) {
                charset = this.method.getParams().getHttpElementCharset();
            }
            footers = HttpParser.parseHeaders(in, charset);
        } catch(HttpException e) {
            LOG.log(Level.SEVERE, "Error parsing trailer headers", e);
            IOException ioe = new IOException(e.getMessage());
            ExceptionUtil.initCause(ioe, e);
            throw ioe;
        }
        if (this.method != null) {
            for (int i = 0; i < footers.length; i++) {
                this.method.addResponseFooter(footers[i]);
            }
        }
    } */

    /**
     * Upon close, this reads the remainder of the chunked message,
     * leaving the underlying socket at a position to start reading the
     * next response without scanning.
     * @throws IOException If an IO problem occurs.
     */
    public void close() throws IOException {
        if (!closed) {
            try {
                if (!eof) {
                    exhaustInputStream(this);
                }
            } finally {
                eof = true;
                closed = true;
            }
        }
    }

    /**
     * Exhaust an input stream, reading until EOF has been encountered.
     *
     * <p>Note that this function is intended as a non-public utility.
     * This is a little weird, but it seemed silly to make a utility
     * class for this one function, so instead it is just static and
     * shared that way.</p>
     *
     * @param inStream The {@link InputStream} to exhaust.
     * @throws IOException If an IO problem occurs
     */
    static void exhaustInputStream(InputStream inStream)
        throws IOException {
        // read and discard the remainder of the message
        byte[] buffer = new byte[1024];

        while (inStream.read(buffer) >= 0) {
            ;
        }
    }
}
