// Copyright (c) 1996-2002 Brian D. Carlstrom

package bdc.util;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.logging.Level;

/**
*/
public final class IOUtil
{
    /*-- Stream Utilities ---------------------------------------------------*/
    /*
        text   output = printWriter
        text    input = bufferedReader
        binary output = bufferedOutputStream
        binary  input = bufferedInputStream
    */

    /**
        Opens a text file with a reasonable stream interface on top of a
        buffered layer.

        @param file the file to open
        @param encoding the encoding to use

        @return a buffered PrintWriter wrapped around the file

        @exception IOException if there was an error opening the file
    */
    public static PrintWriter printWriter (File file, String encoding)
      throws IOException
    {
        return printWriter(new FileOutputStream(file), encoding);
    }


    /**
        Open a text stream over a binary one on top of a buffered
        layer.

    */
    public static PrintWriter printWriter (OutputStream o, String encoding)
      throws IOException
    {
        return new PrintWriter(
            new BufferedWriter(
                new OutputStreamWriter(o, encoding)));
    }

    /**
        Opens a text file on top of a buffered layer.

        @param file the File to opened
        @param encoding the encoding to use

        @return a BufferedReader around the file

        @exception IOException if there was an error opening the file
    */
    public static BufferedReader bufferedReader (File file, String encoding)
      throws IOException
    {
        return bufferedReader(new FileInputStream(file.getPath()), encoding);
    }

    /**
        Opens a text stream over a binary one on top of a buffered
        layer.

    */
    public static BufferedReader bufferedReader (InputStream i,
                                                 String      encoding)
      throws IOException
    {
        return new BufferedReader(new InputStreamReader(i, encoding));
    }

    /**
        Opens a binary file on top of a buffered layer.

        @param file the file to open

        @return a BufferedInputStream wrapping the file

        @exception IOException if there was an error opening the file
    */
    public static BufferedInputStream bufferedInputStream (File file)
      throws IOException
    {
        return new BufferedInputStream(new FileInputStream(file));
    }

    /**
        Opens a binary file on top of a buffered layer.

        @param file the file to open

        @return a BufferedOutputStream wrapping the file

        @exception IOException if there was an error opening the file
    */
    public static BufferedOutputStream bufferedOutputStream (File file)
      throws IOException
    {
        return new BufferedOutputStream(new FileOutputStream(file));
    }


    /**
        Read a line from an InputStream.

        @param in the stream to read from

        @return the line read, without the newline, or <b>null</b> if
        there is no more data

        @exception IOException if there was an error in reading from
        the stream
    */
    public static String readLine (InputStream in) throws IOException
    {
        return readLine(in, new char[128]);
    }

    /**
        Read a line from an InputStream.

        @param in the stream to read from
        @param lineBuffer a buffer to use for reading the
        lines. Performance is better if the buffer is big enough for
        the longest line.

        @return the line read, without the newline, or <b>null</b> if
        there is no more data

        @exception IOException if there was an error in reading from
        the stream
    */
    public static String readLine (InputStream in,
                                   char[]      lineBuffer)
      throws IOException
    {
        char buf[] = lineBuffer;

        int room = buf.length;
        int offset = 0;
        int c;

    loop:
        while (true) {
            switch (c = in.read()) {
              case -1:
              case '\n': {
                  break loop;
              }

              case '\r': {
                  int c2 = in.read();
                  if (c2 != '\n') {
                      throw new IOException(
                          "Expected newline after carriage return");
                  }
                  break loop;
              }

              default: {
                  if (--room < 0) {
                      buf = new char[offset + 128];
                      room = buf.length - offset - 1;
                      System.arraycopy(lineBuffer, 0, buf, 0, offset);
                      lineBuffer = buf;
                  }
                  buf[offset++] = (char)c;
                  break;
              }
            }
        }
        if ((c == -1) && (offset == 0)) {
            return null;
        }
        return String.copyValueOf(buf, 0, offset);
    }

    /**
        Copy a file if the source is newer than the target

        @param sourceFile the source of the copy
        @param targetFile the target of the copy

        @return <b>false</b> if there was a problem.
    */
    public static boolean copyFileIfNewer (File sourceFile, File targetFile)
    {
        long sourceModified = sourceFile.lastModified();
        long targetModified = targetFile.lastModified();
        if (!targetFile.exists() || sourceModified > targetModified) {
            return copyFile(sourceFile, targetFile);
        }
        return true;
    }

    /**
        Copy a file.

        @param sourceFile the source
        @param targetFile the target

        @return <b>false</b> if there was a problem.
    */
    public static boolean copyFile (File sourceFile, File targetFile)
    {
        char [] buffer = new char [2048];
        return copyFile(sourceFile, targetFile, buffer);
    }

    /**
        Copy a file.

        @param sourceFile the source of the copy
        @param targetFile the target of the copy
        @param buffer a buffer to use for the copy

        @return <b>false</b> if there was a problem.
    */
    public static boolean copyFile (File sourceFile, File targetFile,
                                    char [] buffer)
    {
        int bufferSize = buffer.length;
        boolean success = false;
        Reader reader = null;
        Writer writer = null;
        try {
            reader = new BufferedReader(new FileReader(sourceFile), bufferSize);
            writer = new BufferedWriter(new FileWriter(targetFile), bufferSize);
            success = readerToWriter(reader, writer, buffer);
        }
        catch (IOException ioe) {
            success = false;
            Log.util.log(
                Level.SEVERE,
                "Failed to copy file.  Source: {0}, Target: {1}, error {2}",
                new Object[] {sourceFile, targetFile, ioe}
                );
        }
            // the following code try's very hard (forgive the pun) to
            // close both files
        finally {
            try {
                if (writer != null) {
                    writer.close();
                }
            }
            catch (IOException ioe) {
                Log.util.log(
                    Level.SEVERE,
                    "Error while closing file {0}.  Error {1}.",
                    new Object[] {targetFile, ioe}
                    );
                success = false;
            }
            finally {
                try {
                    if (reader != null) {
                        reader.close();
                    }
                }
                catch (IOException ioe) {
                    Log.util.log(
                        Level.SEVERE,
                        "Error while closing file {0}.  Error {1}.",
                        new Object[] {sourceFile, ioe}
                        );
                    success = false;
                }
            }
        }
        return success;
    }

    /**
        Copy Reader to Writer.

        @param reader the source of the copy
        @param writer the destination of the copy, null if none

        @return <b>false</b> if there was a problem.
    */
    public static boolean readerToWriter (Reader reader,
                                          Writer writer)
    {
        return readerToWriter(reader,
                              writer,
                              new char[2048]);
    }

    /**
        Copy Reader to Writer.

        @param reader the source of the copy
        @param writer the destination of the copy, null if none
        @param buffer a buffer to use for the copy

        @return <b>false</b> if there was a problem.
    */
    public static boolean readerToWriter (Reader reader,
                                          Writer writer,
                                          char[] buffer)
    {
        Assert.that(buffer != null, "Char [] buffer must not be null.");
        try {
            int bufferSize = buffer.length;
            int charsRead = reader.read(buffer, 0, bufferSize);

            while (charsRead > 0) {
                if (writer != null) {
                    writer.write(buffer, 0, charsRead);
                }
                charsRead = reader.read(buffer, 0, bufferSize);
            }
        }
        catch (IOException ioe) {
            return false;
        }
        return true;
    }

    /**
        Copies all data from an InputStream to an OutputStream.

        @param input the source of the copy
        @param output the destination of the copy, null if none

        @return <b>false</b> if there was a problem.
    */
    public static boolean inputStreamToOutputStream (
        InputStream input, OutputStream output)
    {
        byte[] buffer = new byte[2048];
        return inputStreamToOutputStream(input, output, buffer, false);
    }

    /**
        Copies all data from an InputStream to an OutputStream.

        @param input the source of the copy
        @param output the destination of the copy, null if none
        @param buffer a buffer to use for copying
        @param flush if <b>true</b> <b>output</b> is flushed after
        every write call

        @return <b>false</b> if there was a problem.
    */
    public static boolean inputStreamToOutputStream (
        InputStream input, OutputStream output, byte[] buffer, boolean flush)
    {
        try {
            inputStreamToOutputStream(input, output, buffer, flush,
                                      Integer.MAX_VALUE);
        }
        catch (IOException ioe) {
            return false;
        }
        return true;
    }


    /**
        Copies the specified amount of data from an InputStream to an
        OutputStream.

        @param input the source of the copy
        @param output the destination of the copy, null if none
        @param buffer a buffer to use for copying
        @param flush if <b>true</b> <b>output</b> is flushed after
        every write call
        @param bytesToCopy the number of bytes to copy

        @return the number of bytes copied
    */
    public static int inputStreamToOutputStream (
        InputStream input,
        OutputStream output,
        byte[] buffer,
        boolean flush,
        int bytesToCopy) throws IOException
    {
        if (output == null) {
            flush = false;
        }

        int bytesWritten = 0;
        int bufferSize = buffer.length;
        int bytesToRead = Math.min(bufferSize, bytesToCopy);
        int bytesRead = input.read(buffer, 0, bytesToRead);
        while (bytesRead > 0 && bytesWritten != bytesToCopy) {
            if (output != null) {
                output.write(buffer, 0, bytesRead);
            }
            bytesWritten += bytesRead;
            if (flush) {
                output.flush();
            }
            bytesToRead = Math.min(bufferSize, bytesToCopy - bytesWritten);
            if (bytesToRead != 0) {
                bytesRead = input.read(buffer, 0, bytesToRead);
            }
        }
        return bytesWritten;
    }

    /**
        Read the contents of a URL as a byte array
.

        @param url the URL to read
        @return the contents of the URL, or null if there
        are any problems
    */
    public static byte[] bytesFromURL (URL url)
    {
        try {
            InputStream urlStream = url.openStream();
            int available = urlStream.available();
            byte[] buffer = new byte[available];
            urlStream.read(buffer);
            urlStream.close();

            return buffer;
        }
        catch(IOException ioe) {
            return null;
        }
    }

    /**
        Read the contents of a URL as a string.

        @param url the URL to read
        @param encoding the encoding to use (as in a String constructor).
        If encoding is null, we use the default encoding
        @return the contents of the URL, or null if there
        are any problems
    */
    public static String stringFromURL (URL url, String encoding)
    {
        byte[] buffer = bytesFromURL(url);
        if (buffer != null) {
            if (encoding == null) {
                return new String(buffer);
            }
            else {
                try {
                    return new String(buffer, encoding);
                }
                catch (UnsupportedEncodingException uee) {
                    return null;
                }
            }
        }

        return null;
    }
  static final int BufferSize = 4096;
    /**
        Copy a URL (String format) to File.

        @param sourceURLString the URL string reference source content
        @param targetFile the destination for the content
        @exception FileNotFoundException
        @exception MalformedURLException
        @exception IOException
    */
    public static void copyUrlToFile (String sourceURLString, File targetFile)
      throws FileNotFoundException, MalformedURLException, IOException
    {
        InputStream in = null;
        OutputStream out = null;

        try {
            URL url = new URL(sourceURLString);
            URLConnection urlConnection = url.openConnection();
            urlConnection.setDoInput(true);
            in = urlConnection.getInputStream();

            out = IOUtil.bufferedOutputStream(targetFile);

            byte[] buffer = new byte[BufferSize];
            int bytesRead = in.read(buffer, 0, BufferSize);

            while (bytesRead > 0) {
                out.write(buffer, 0, bytesRead);
                bytesRead = in.read(buffer, 0, BufferSize);
            }
        }
        finally {
            try {
                if (in != null) {
                    in.close();
                }
            }
            finally {
                if (out != null) {
                    out.close();
                }
            }
        }
    }

    /**
        prevent people from creating instances of this class
    */
    private IOUtil ()
    {
    }
}
