/***************************************
 *                                     *
 *  JBoss: The OpenSource J2EE WebOS   *
 *                                     *
 *  Distributable under LGPL license.  *
 *  See terms of license at gnu.org.   *
 *                                     *
 ***************************************/

package org.jboss.util.stream;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;

import org.jboss.util.ThrowableHandler;

import org.jboss.logging.Logger;

/**
 * A collection of stream related utility methods.
 *
 * <p>Exceptions that are thrown and not explicitly declared are given to
 *    the {@link ThrowableHandler} for further processing.
 *
 * @version <tt>$Revision: 1.4 $</tt>
 * @author  <a href="mailto:jason@planet57.com">Jason Dillon</a>
 */
public final class Streams
{
   private static final Logger log = Logger.getLogger(Streams.class);
   
   /////////////////////////////////////////////////////////////////////////
   //                               Closing                               //
   /////////////////////////////////////////////////////////////////////////

   /**
    * Attempt to close an <tt>InputStream</tt>.
    *
    * @param stream  <tt>InputStream</tt> to attempt to close.
    * @return        <tt>True</tt> if stream was closed (or stream was null),
    *                or <tt>false</tt> if an exception was thrown.
    */
   public static boolean close(final InputStream stream) {
      // do not attempt to close null stream, but return sucess
      if (stream == null) {
         return true;
      }
      
      boolean success = true;

      try {
         stream.close();
      }
      catch (IOException e) {
         success = false;
         ThrowableHandler.add(e);
      }

      return success;
   }

   /**
    * Attempt to close an <tt>OutputStream</tt>.
    *
    * @param stream  <tt>OutputStream</tt> to attempt to close.
    * @return        <tt>True</tt> if stream was closed (or stream was null),
    *                or <tt>false</tt> if an exception was thrown.
    */
   public static boolean close(final OutputStream stream) {
      // do not attempt to close null stream, but return sucess
      if (stream == null) {
         return true;
      }

      boolean success = true;

      try {
         stream.close();
      }
      catch (IOException e) {
         success = false;
         ThrowableHandler.add(e);
      }

      return success;
   }

   /**
    * Attempt to close an <tt>InputStream</tt> or <tt>OutputStream</tt>.
    *
    * @param stream  Stream to attempt to close.
    * @return        <tt>True</tt> if stream was closed (or stream was null),
    *                or <tt>false</tt> if an exception was thrown.
    *
    * @throws IllegalArgumentException    Stream is not an <tt>InputStream</tt>
    *                                     or <tt>OuputStream</tt>.
    */
   public static boolean close(final Object stream) {
      boolean success = false;

      if (stream instanceof InputStream) {
         success = close((InputStream)stream);
      }
      else if (stream instanceof OutputStream) {
         success = close((OutputStream)stream);
      }
      else {
         throw new IllegalArgumentException
            ("stream is not an InputStream or OutputStream");
      }

      return success;
   }

   /**
    * Attempt to close an array of <tt>InputStream</tt>s.
    *
    * @param streams Array of <tt>InputStream</tt>s to attempt to close.
    * @return        <tt>True</tt> if all streams were closed, or <tt>false</tt>
    *                if an exception was thrown.
    */
   public static boolean close(final InputStream[] streams) {
      boolean success = true;

      for (int i=0; i<streams.length; i++) {
         boolean rv = close(streams[i]);
         if (!rv) success = false;
      }

      return success;
   }

   /**
    * Attempt to close an array of <tt>OutputStream</tt>s.
    *
    * @param streams Array of <tt>OutputStream</tt>s to attempt to close.
    * @return        <tt>True</tt> if all streams were closed, or <tt>false</tt>
    *                if an exception was thrown.
    */
   public static boolean close(final OutputStream[] streams) {
      boolean success = true;

      for (int i=0; i<streams.length; i++) {
         boolean rv = close(streams[i]);
         if (!rv) success = false;
      }

      return success;
   }

   /**
    * Attempt to close an array of <tt>InputStream</tt>a and/or 
    * <tt>OutputStream</tt>s.
    *
    * @param streams Array of streams to attempt to close.
    * @return        <tt>True</tt> if all streams were closed, or <tt>false</tt>
    *                if an exception was thrown.
    *
    * @throws IllegalArgumentException    Stream is not an <tt>InputStream</tt>
    *                                     or <tt>OuputStream</tt>.  Closing 
    *                                     stops at the last valid stream
    *                                     object in this case.
    */
   public static boolean close(final Object[] streams) {
      boolean success = true;

      for (int i=0; i<streams.length; i++) {
         boolean rv = close(streams[i]);
         if (!rv) success = false;
      }

      return success;
   }

   /**
    * Attempt to flush and close an <tt>OutputStream</tt>.
    *
    * @param stream  <tt>OutputStream</tt> to attempt to flush and close.
    * @return        <tt>True</tt> if stream was flushed and closed, or
    *                <tt>false</tt> if an exception was thrown.
    */
   public static boolean fclose(final OutputStream stream) {
       return flush(stream) && close(stream);
   }

   /**
    * Attempt to flush and close an array of <tt>OutputStream</tt>s.
    *
    * @param streams  <tt>OutputStream</tt>s to attempt to flush and close.
    * @return         <tt>True</tt> if all streams were flushed and closed, 
    *                 or <tt>false</tt> if an exception was thrown.
    */
   public static boolean fclose(final OutputStream[] streams) {
      boolean success = true;

      for (int i=0; i<streams.length; i++) {
         boolean rv = fclose(streams[i]); 
         if (!rv) success = false;
      }

      return success;
   }
    

   /////////////////////////////////////////////////////////////////////////
   //                                Flushing                             //
   /////////////////////////////////////////////////////////////////////////

   /**
    * Attempt to flush an <tt>OutputStream</tt>.
    *
    * @param stream  <tt>OutputStream</tt> to attempt to flush.
    * @return        <tt>True</tt> if stream was flushed (or stream was null),
    *                or <tt>false</tt> if an exception was thrown.
    */
   public static boolean flush(final OutputStream stream) {
      // do not attempt to close null stream, but return sucess
      if (stream == null) {
         return true;
      }
      
      boolean success = true;

      try {
         stream.flush();
      }
      catch (IOException e) {
         success = false;
         ThrowableHandler.add(e);
      }

      return success;
   }

   /**
    * Attempt to flush an array of <tt>OutputStream</tt>s.
    *
    * @param streams <tt>OutputStream</tt>s to attempt to flush.
    * @return        <tt>True</tt> if all streams were flushed, or <tt>false</tt>
    *                 if an exception was thrown.
    */
   public static boolean flush(final OutputStream[] streams) {
      boolean success = true;

      for (int i=0; i<streams.length; i++) {
         boolean rv = flush(streams[i]);
         if (!rv) success = false;
      }

      return success;
   }


   /////////////////////////////////////////////////////////////////////////
   //                                  Misc                               //
   /////////////////////////////////////////////////////////////////////////

   /** The default buffer size that will be used for buffered operations. */
   public static final int DEFAULT_BUFFER_SIZE = 2048;

   /**
    * Copy all of the bytes from the input stream to the output stream.
    *
    * @param input   Stream to read bytes from.
    * @param output  Stream to write bytes to.
    * @param buffer  The buffer to use while copying.
    * @return        The total number of bytes copied.
    *
    * @throws IOException  Failed to copy bytes.
    */
   public static long copy(final InputStream input, 
                           final OutputStream output, 
                           final byte buffer[])
      throws IOException
   {
      long total = 0;
      int read;

      boolean trace = log.isTraceEnabled();
      if (trace) {
         log.trace("copying " + input + " to " + output + " with buffer size: " + buffer.length);
      }
      
      while ((read = input.read(buffer)) != -1) {
         output.write(buffer, 0, read);
         total += read;

         if (trace) {
            log.trace("bytes read: " + read + "; total bytes read: " + total);
         }
      }

      return total;
   }

   /**
    * Copy all of the bytes from the input stream to the output stream.
    *
    * @param input   Stream to read bytes from.
    * @param output  Stream to write bytes to.
    * @param size    The size of the buffer to use while copying.
    * @return        The total number of bytes copied.
    *
    * @throws IOException  Failed to copy bytes.
    */
   public static long copy(final InputStream input, 
                           final OutputStream output, 
                           final int size)
      throws IOException
   {
      return copy(input, output, new byte[size]);
   }

   /**
    * Copy all of the bytes from the input stream to the output stream.
    *
    * @param input   Stream to read bytes from.
    * @param output  Stream to write bytes to.
    * @return        The total number of bytes copied.
    *
    * @throws IOException  Failed to copy bytes.
    */
   public static long copy(final InputStream input, 
                           final OutputStream output)
      throws IOException
   {
      return copy(input, output, DEFAULT_BUFFER_SIZE);
   }

   /**
    * Copy all of the bytes from the input stream to the output stream
    * wrapping streams in buffers as needed.
    *
    * @param input   Stream to read bytes from.
    * @param output  Stream to write bytes to.
    * @return        The total number of bytes copied.
    *
    * @throws IOException  Failed to copy bytes.
    */
   public static long copyb(InputStream input, 
                            OutputStream output)
      throws IOException
   {
      if (!(input instanceof BufferedInputStream)) {
         input = new BufferedInputStream(input);
      }
      
      if (!(output instanceof BufferedOutputStream)) {
         output = new BufferedOutputStream(output);
      }

      long bytes = copy(input, output, DEFAULT_BUFFER_SIZE);

      output.flush();

      return bytes;
   }
   
   /**
    * Copy a limited number of bytes from the input stream to the 
    * output stream.
    *
    * @param input   Stream to read bytes from.
    * @param output  Stream to write bytes to.
    * @param buffer  The buffer to use while copying.
    * @param length  The maximum number of bytes to copy.
    * @return        The total number of bytes copied.
    *
    * @throws IOException  Failed to copy bytes.
    */
   public static long copySome(final InputStream input, 
                               final OutputStream output, 
                               final byte buffer[],
                               final long length)
      throws IOException
   {
      long total = 0;
      int read;
      int readLength;

      boolean trace = log.isTraceEnabled();
      
      // setup the initial readLength, if length is less than the buffer
      // size, then we only want to read that much
      readLength = Math.min((int)length, buffer.length);
      if (trace) {
         log.trace("initial read length: " + readLength);
      }

      while (readLength != 0 && (read = input.read(buffer, 0, readLength)) != -1) 
      {
         if (trace) log.trace("read bytes: " + read);
         output.write(buffer, 0, read);
         total += read;
         if (trace) log.trace("total bytes read: " + total);

         // update the readLength
         readLength = Math.min((int)(length - total), buffer.length);
         if (trace) log.trace("next read length: " + readLength);
      }

      return total;
   }

   /**
    * Copy a limited number of bytes from the input stream to the 
    * output stream.
    *
    * @param input   Stream to read bytes from.
    * @param output  Stream to write bytes to.
    * @param size    The size of the buffer to use while copying.
    * @param length  The maximum number of bytes to copy.
    * @return        The total number of bytes copied.
    *
    * @throws IOException  Failed to copy bytes.
    */
   public static long copySome(final InputStream input, 
                               final OutputStream output, 
                               final int size,
                               final long length)
      throws IOException
   {
      return copySome(input, output, new byte[size], length);
   }

   /**
    * Copy a limited number of bytes from the input stream to the 
    * output stream.
    *
    * @param input   Stream to read bytes from.
    * @param output  Stream to write bytes to.
    * @param length  The maximum number of bytes to copy.
    * @return        The total number of bytes copied.
    *
    * @throws IOException  Failed to copy bytes.
    */
   public static long copySome(final InputStream input, 
                               final OutputStream output, 
                               final long length)
      throws IOException
   {
      return copySome(input, output, DEFAULT_BUFFER_SIZE, length);
   }
}