/**
 * version history:
 * 1.4 2007-09-26 add TIMEOUT
 * 1.3 2007-08-24 readStringBlocking, readBytesBlocking
 * 1.5 2007-12-30 add alternate get and post methods that take a full URL.
 * 1.6 2008-01-14 add gzip option on read
 */
package com.mindprod.http;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.zip.GZIPOutputStream;

/**
 * Read from a server. See com.mindprod.submitter for sample code to use this class.
 *
 * @author Roedy Green, Canadian Mind Products
 * @version 1.6 2008-01-14 add gzip option on read.
 */
@SuppressWarnings( { "WeakerAccess" } )
public final class Read
    {
    // ------------------------------ FIELDS ------------------------------

    /**
     * characters should be arriving within a millisecond in ordinary circumstances. In order to avoid hogging at the
     * CPU in a mad loop to read non-existence characters, we sleep and try again later. Time to sleep in ms.
     */
    private static final int SLEEP_TIME = 100/* 0.1 sec = 100 ms */;
    // -------------------------- PUBLIC STATIC METHODS --------------------------


    /**
     * Reads exactly len bytes from the input stream into the byte array. This method reads repeatedly from the
     * underlying stream until all the bytes are read. InputStream.read is often documented to block like this, but in
     * actuality it does not always do so, and returns early with just a few bytes. readBytesBlocking blocks until all
     * the bytes are read, the end of the stream is detected, or an exception is thrown. You will always get as many
     * bytes as you asked for unless you get an eof or other exception. Unlike readFully, you find out how many bytes
     * you did get. Formerly called readBlocking.
     *
     * @param in              stream to read
     * @param b               the buffer into which the data is read.
     * @param off             the start offset of the data in the array, not offset into the file!
     * @param len             the number of bytes to read.
     * @param timeoutInMillis give up after this amount of time.
     * @return number of bytes actually read.
     * @throws IOException if an I/O error occurs, usually a timeout.
     */
    @SuppressWarnings( { "NestedAssignment", "EmptyCatchBlock" } )
    public static int readBytesBlocking( InputStream in,
                                         byte b[],
                                         int off,
                                         int len,
                                         int timeoutInMillis ) throws IOException
        {
        int totalBytesRead = 0;
        int bytesRead;
        long giveup = System.currentTimeMillis() + timeoutInMillis;
        while ( totalBytesRead < len
                && ( bytesRead =
                in.read( b, off + totalBytesRead, len - totalBytesRead ) )
                   >= 0 )
            {
            if ( bytesRead == 0 )
                {
                try
                    {
                    if ( System.currentTimeMillis() >= giveup )
                        {
                        throw new IOException( "timeout" );
                        }
                    // don't hammer the system and suck up all the CPU
                    // beating a tight loop when there are no chars.
                    // If this keeps up we may trigger a java.net.SocketTimeoutException exception.
                    Thread.sleep( SLEEP_TIME );
                    }
                catch ( InterruptedException e )
                    {
                    }
                }
            else
                {
                totalBytesRead += bytesRead;
                giveup = System.currentTimeMillis() + timeoutInMillis;
                }
            }
        return totalBytesRead;
        }// end readBytesBlocking

    /**
     * Used to read until EOF on an Inputstream that sometimes returns 0 bytes because data have not arrived yet. Does
     * not close the stream. Formerly called readEverything.
     *
     * @param is              InputStream to read from.
     * @param estimatedLength Estimated number of <b>bytes</b> that will be read. -1 or 0 mean you have no idea. Best to
     *                        make some sort of guess a little on the high side.
     * @param timeoutInMillis give up after this amount of time.
     * @param gzipped         true if the bytes are compressed with gzip. Request decompression.
     * @param encoding        The encoding of the byte stream. readStringBlocking converts to a standard Unicode-16
     *                        String. usually UTF-8 or ISO-8859-1.
     * @return String representing the contents of the entire stream.
     * @throws IOException if connection lost, timeout etc., possibly UnsupportedEncodingException If the named charset
     *                     is not supported
     */
    @SuppressWarnings( { "NestedAssignment", "EmptyCatchBlock" } )
    public static String readStringBlocking( InputStream is,
                                             int estimatedLength,
                                             int timeoutInMillis,
                                             boolean gzipped,
                                             String encoding ) throws IOException
        {
        if ( estimatedLength <= 0 )
            {
            estimatedLength = 10 * 1024;
            }

        final int chunkSize = Math.min( estimatedLength, 4 * 1024 );
        final byte[] ba = new byte[chunkSize];
        // will grow as needed.

        // O P E N
        final ByteArrayOutputStream baos = new ByteArrayOutputStream( estimatedLength + 1024 );

        final GZIPOutputStream gzos;
        if ( gzipped )
            {
            gzos = new GZIPOutputStream( baos, 4096/* buffsize in bytes */ );
            }
        else
            {
            gzos = null;
            }

        // -1 means eof, 0 means none available for now.
        int bytesRead;
        long giveup = System.currentTimeMillis() + timeoutInMillis;
        while ( ( bytesRead = is.read( ba, 0, chunkSize ) ) >= 0 )
            {
            if ( bytesRead == 0 )
                {
                try
                    {
                    if ( System.currentTimeMillis() >= giveup )
                        {
                        throw new IOException( "timeout" );
                        }
                    // no data for now
                    // wait a while before trying again to see if data has arrived.
                    // avoid hogging cpu in a tight loop
                    Thread.sleep( SLEEP_TIME );
                    }
                catch ( InterruptedException e )
                    {
                    }
                }
            else
                {
                // got some data. Tack it on the end of our ByteArrayOutputStream
                if ( gzipped )
                    {
                    gzos.write( ba, 0, bytesRead );
                    }
                else
                    {
                    baos.write( ba, 0, bytesRead );
                    }
                // start the timeout over, every time we get some data.
                giveup = System.currentTimeMillis() + timeoutInMillis;
                }
            }

        if ( gzipped )
            {
            // C L O S E
            // this code is not yet tested.  It might prove necessary to do the toArray prior to the gzos.close.
            gzos.finish();
            gzos.close();
            return new String( baos.toByteArray(), encoding );
            }
        else
            {
            return baos.toString( encoding );
            }// end readStringBlocking
        }

    // --------------------------- CONSTRUCTORS ---------------------------

    /**
     * Static only.  Prevent instantiation.
     */
    private Read()
        {
        }
    }