/*
 *   Licensed to the Apache Software Foundation (ASF) under one
 *   or more contributor license agreements.  See the NOTICE file
 *   distributed with this work for additional information
 *   regarding copyright ownership.  The ASF licenses this file
 *   to you under the Apache License, Version 2.0 (the
 *   "License"); you may not use this file except in compliance
 *   with the License.  You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing,
 *   software distributed under the License is distributed on an
 *   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *   KIND, either express or implied.  See the License for the
 *   specific language governing permissions and limitations
 *   under the License.
 *
 */

package org.apache.directory.api.util;


import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;

import org.apache.directory.api.i18n.I18n;


/**
 * This code comes from Apache commons.io library.
 * 
 * Origin of code: Excalibur, Alexandria, Tomcat, Commons-Utils.
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public final class IOUtils
{
    /**
     * The default buffer size ({@value}) to use for
     * {@link #copyLarge(InputStream, OutputStream)}
     * and
     * {@link #copyLarge(Reader, Writer)}
     */
    private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;

    /** The end of file */
    private static final int EOF = -1;


    /**
     * Creates a new instance of FileUtils.
     */
    private IOUtils()
    {
        // Nothing to do.
    }


    /**
    * Closes an <code>InputStream</code> unconditionally.
    * <p>
    * Equivalent to {@link InputStream#close()}, except any exceptions will be ignored.
    * This is typically used in finally blocks.
    * <p>
    * Example code:
    * <pre>
    *   byte[] data = new byte[1024];
    *   InputStream in = null;
    *   try {
    *       in = new FileInputStream("foo.txt");
    *       in.read(data);
    *       in.close(); //close errors are handled
    *   } catch (Exception e) {
    *       // error handling
    *   } finally {
    *       IOUtils.closeQuietly(in);
    *   }
    * </pre>
    *
    * @param input  the InputStream to close, may be null or already closed
    */
    public static void closeQuietly( InputStream input )
    {
        closeQuietly( ( Closeable ) input );
    }


    /**
     * Closes a <code>Closeable</code> unconditionally.
     * <p>
     * Equivalent to {@link Closeable#close()}, except any exceptions will be ignored. This is typically used in
     * finally blocks.
     * <p>
     * Example code:
     * 
     * <pre>
     * Closeable closeable = null;
     * try {
     *     closeable = new FileReader(&quot;foo.txt&quot;);
     *     // process closeable
     *     closeable.close();
     * } catch (Exception e) {
     *     // error handling
     * } finally {
     *     IOUtils.closeQuietly(closeable);
     * }
     * </pre>
     * 
     * Closing all streams:
     * 
     * <pre>
     * try {
     *     return IOUtils.copy(inputStream, outputStream);
     * } finally {
     *     IOUtils.closeQuietly(inputStream);
     *     IOUtils.closeQuietly(outputStream);
     * }
     * </pre>
     * 
     * @param closeables the objects to close, may be null or already closed
     * @since 2.5
     */
    public static void closeQuietly( Closeable... closeables )
    {
        if ( closeables == null )
        {
            return;
        }

        for ( Closeable closeable : closeables )
        {
            closeQuietly( closeable );
        }
    }


    /**
     * Closes a <code>Closeable</code> unconditionally.
     * <p>
     * Equivalent to {@link Closeable#close()}, except any exceptions will be ignored. This is typically used in
     * finally blocks.
     * <p>
     * Example code:
     * 
     * <pre>
     * Closeable closeable = null;
     * try {
     *     closeable = new FileReader(&quot;foo.txt&quot;);
     *     // process closeable
     *     closeable.close();
     * } catch (Exception e) {
     *     // error handling
     * } finally {
     *     IOUtils.closeQuietly(closeable);
     * }
     * </pre>
     * 
     * Closing all streams:
     * 
     * <pre>
     * try {
     *     return IOUtils.copy(inputStream, outputStream);
     * } finally {
     *     IOUtils.closeQuietly(inputStream);
     *     IOUtils.closeQuietly(outputStream);
     * }
     * </pre>
     * 
     * @param closeable
     *            the objects to close, may be null or already closed
     * @since 2.0
     */
    public static void closeQuietly( Closeable closeable )
    {
        try
        {
            if ( closeable != null )
            {
                closeable.close();
            }
        }
        catch ( IOException ioe )
        {
            // ignore
        }
    }


    /**
    * Gets the contents of an <code>InputStream</code> as a String
    * using the specified character encoding.
    * <p>
    * This method buffers the input internally, so there is no need to use a
    * <code>BufferedInputStream</code>.
    * </p>
    * @param input  the <code>InputStream</code> to read from
    * @param encoding  the encoding to use, null means platform default
    * @return the requested String
    * @throws NullPointerException if the input is null
    * @throws IOException if an I/O error occurs
    * @since 2.3
    */
    public static String toString( InputStream input, Charset encoding ) throws IOException
    {
        StringBuilderWriter sw = new StringBuilderWriter();
        copy( input, sw, encoding );

        return sw.toString();
    }


    /**
     * Returns the given Charset or the default Charset if the given Charset is null.
     * 
     * @param charset A charset or null.
     * @return the given Charset or the default Charset if the given Charset is null
     */
    public static Charset toCharset( Charset charset )
    {
        return charset == null ? Charset.defaultCharset() : charset;
    }


    /**
     * Returns a Charset for the named charset. If the name is null, return the default Charset.
     * 
     * @param charset The name of the requested charset, may be null.
     * @return a Charset for the named charset
     */
    public static Charset toCharset( String charset )
    {
        return charset == null ? Charset.defaultCharset() : Charset.forName( charset );
    }


    /**
     * Copies bytes from an <code>InputStream</code> to chars on a
     * <code>Writer</code> using the specified character encoding.
     * <p>
     * This method buffers the input internally, so there is no need to use a
     * <code>BufferedInputStream</code>.
     * <p>
     * This method uses {@link InputStreamReader}.
     *
     * @param input  the <code>InputStream</code> to read from
     * @param output  the <code>Writer</code> to write to
     * @param inputEncoding  the encoding to use for the input stream, null means platform default
     * @throws NullPointerException if the input or output is null
     * @throws IOException if an I/O error occurs
     * @since 2.3
     */
    public static void copy( InputStream input, Writer output, Charset inputEncoding ) throws IOException
    {
        InputStreamReader in = new InputStreamReader( input, toCharset( inputEncoding ) );
        copy( in, output );
    }


    /**
     * Copies chars from a <code>Reader</code> to a <code>Writer</code>.
     * <p>
     * This method buffers the input internally, so there is no need to use a
     * <code>BufferedReader</code>.
     * <p>
     * Large streams (over 2GB) will return a chars copied value of
     * <code>-1</code> after the copy has completed since the correct
     * number of chars cannot be returned as an int. For large streams
     * use the <code>copyLarge(Reader, Writer)</code> method.
     *
     * @param input  the <code>Reader</code> to read from
     * @param output  the <code>Writer</code> to write to
     * @return the number of characters copied, or -1 if &gt; Integer.MAX_VALUE
     * @throws NullPointerException if the input or output is null
     * @throws IOException if an I/O error occurs
     * @since 1.1
     */
    public static int copy( Reader input, Writer output ) throws IOException
    {
        long count = copyLarge( input, output );

        if ( count > Integer.MAX_VALUE )
        {
            return -1;
        }

        return ( int ) count;
    }

    
    /**
     * Copies bytes from an <code>InputStream</code> to an
     * <code>OutputStream</code>.
     * <p>
     * This method buffers the input internally, so there is no need to use a
     * <code>BufferedInputStream</code>.
     * <p>
     * Large streams (over 2GB) will return a bytes copied value of
     * <code>-1</code> after the copy has completed since the correct
     * number of bytes cannot be returned as an int. For large streams
     * use the <code>copyLarge(InputStream, OutputStream)</code> method.
     *
     * @param input  the <code>InputStream</code> to read from
     * @param output  the <code>OutputStream</code> to write to
     * @return the number of bytes copied, or -1 if &gt; Integer.MAX_VALUE
     * @throws NullPointerException if the input or output is null
     * @throws IOException if an I/O error occurs
     * @since 1.1
     */
    public static int copy( InputStream input, OutputStream output ) throws IOException 
    {
        long count = copyLarge( input, output );
        
        if ( count > Integer.MAX_VALUE ) 
        {
            return -1;
        }
        
        return ( int ) count;
    }
    
    
    /**
     * Copies bytes from an <code>InputStream</code> to an <code>OutputStream</code> using an internal buffer of the
     * given size.
     * <p>
     * This method buffers the input internally, so there is no need to use a <code>BufferedInputStream</code>.
     *
     * @param input
     *            the <code>InputStream</code> to read from
     * @param output
     *            the <code>OutputStream</code> to write to
     * @param bufferSize
     *            the bufferSize used to copy from the input to the output
     * @return the number of bytes copied
     * @throws NullPointerException
     *             if the input or output is null
     * @throws IOException
     *             if an I/O error occurs
     * @since 2.5
     */
    public static long copy( InputStream input, OutputStream output, int bufferSize ) throws IOException 
    {
        return copyLarge( input, output, new byte[bufferSize] );
    }


    /**
     * Copies chars from a large (over 2GB) <code>Reader</code> to a <code>Writer</code>.
     * <p>
     * This method buffers the input internally, so there is no need to use a
     * <code>BufferedReader</code>.
     * <p>
     * The buffer size is given by #DEFAULT_BUFFER_SIZE.
     *
     * @param input  the <code>Reader</code> to read from
     * @param output  the <code>Writer</code> to write to
     * @return the number of characters copied
     * @throws NullPointerException if the input or output is null
     * @throws IOException if an I/O error occurs
     * @since 1.3
     */
    public static long copyLarge( Reader input, Writer output ) throws IOException
    {
        return copyLarge( input, output, new char[DEFAULT_BUFFER_SIZE] );
    }
    
    
    /**
     * Copies bytes from a large (over 2GB) <code>InputStream</code> to an
     * <code>OutputStream</code>.
     * <p>
     * This method buffers the input internally, so there is no need to use a
     * <code>BufferedInputStream</code>.
     * <p>
     * The buffer size is given by #DEFAULT_BUFFER_SIZE.
     *
     * @param input  the <code>InputStream</code> to read from
     * @param output  the <code>OutputStream</code> to write to
     * @return the number of bytes copied
     * @throws NullPointerException if the input or output is null
     * @throws IOException if an I/O error occurs
     * @since 1.3
     */
    public static long copyLarge( InputStream input, OutputStream output ) throws IOException 
    {
        return copy( input, output, DEFAULT_BUFFER_SIZE );
    }

    
    /**
     * Copies bytes from a large (over 2GB) <code>InputStream</code> to an
     * <code>OutputStream</code>.
     * <p>
     * This method uses the provided buffer, so there is no need to use a
     * <code>BufferedInputStream</code>.
     *
     * @param input  the <code>InputStream</code> to read from
     * @param output  the <code>OutputStream</code> to write to
     * @param buffer the buffer to use for the copy
     * @return the number of bytes copied
     * @throws NullPointerException if the input or output is null
     * @throws IOException if an I/O error occurs
     * @since 2.2
     */
    public static long copyLarge( InputStream input, OutputStream output, byte[] buffer ) throws IOException 
    {
        long count = 0;
        int n;
        
        while ( EOF != ( n = input.read( buffer ) ) ) 
        {
            output.write( buffer, 0, n );
            count += n;
        }
        
        return count;
    }


    /**
     * Copies chars from a large (over 2GB) <code>Reader</code> to a <code>Writer</code>.
     * <p>
     * This method uses the provided buffer, so there is no need to use a
     * <code>BufferedReader</code>.
     *
     * @param input  the <code>Reader</code> to read from
     * @param output  the <code>Writer</code> to write to
     * @param buffer the buffer to be used for the copy
     * @return the number of characters copied
     * @throws NullPointerException if the input or output is null
     * @throws IOException if an I/O error occurs
     * @since 2.2
     */
    public static long copyLarge( Reader input, Writer output, char[] buffer ) throws IOException
    {
        long count = 0;
        int n;

        while ( EOF != ( n = input.read( buffer ) ) )
        {
            output.write( buffer, 0, n );
            count += n;
        }

        return count;
    }


    /**
     * Writes chars from a <code>String</code> to bytes on an
     * <code>OutputStream</code> using the specified character encoding.
     * <p>
     * This method uses {@link String#getBytes(String)}.
     *
     * @param data  the <code>String</code> to write, null ignored
     * @param output  the <code>OutputStream</code> to write to
     * @param encoding  the encoding to use, null means platform default
     * @throws NullPointerException if output is null
     * @throws IOException if an I/O error occurs
     * @since 2.3
     */
    public static void write( String data, OutputStream output, Charset encoding ) throws IOException
    {
        if ( data != null )
        {
            output.write( data.getBytes( toCharset( encoding ) ) );
        }
    }


    /**
     * Gets the contents of an <code>InputStream</code> as a <code>byte[]</code>.
     * Use this method instead of <code>toByteArray(InputStream)</code>
     * when <code>InputStream</code> size is known
     * @param input the <code>InputStream</code> to read from
     * @param size the size of <code>InputStream</code>
     * @return the requested byte array
     * @throws IOException if an I/O error occurs or <code>InputStream</code> size differ from parameter size
     * @throws IllegalArgumentException if size is less than zero
     * @since 2.1
     */
    public static byte[] toByteArray( InputStream input, int size ) throws IOException
    {
        if ( size < 0 )
        {
            throw new IllegalArgumentException( I18n.err( I18n.ERR_17070_SIZE_POSITIVE, size ) );
        }

        if ( size == 0 )
        {
            return new byte[0];
        }

        byte[] data = new byte[size];
        int offset = 0;
        int readed = input.read( data, offset, size - offset );

        while ( offset < size && ( readed != EOF ) )
        {
            offset += readed;
            readed = input.read( data, offset, size - offset );
        }

        if ( offset != size )
        {
            throw new IOException( I18n.err( I18n.ERR_17071_UNEXPECTED_SIZE, offset, size ) );
        }

        return data;
    }
    
    
    /**
     * Gets contents of an <code>InputStream</code> as a <code>byte[]</code>.
     * Use this method instead of <code>toByteArray(InputStream)</code>
     * when <code>InputStream</code> size is known.
     * <b>NOTE:</b> the method checks that the length can safely be cast to an int without truncation
     * before using {@link IOUtils#toByteArray(java.io.InputStream, int)} to read into the byte array.
     * (Arrays can have no more than Integer.MAX_VALUE entries anyway)
     *
     * @param input the <code>InputStream</code> to read from
     * @param size the size of <code>InputStream</code>
     * @return the requested byte array
     * @throws IOException if an I/O error occurs or <code>InputStream</code> size differ from parameter size
     * @throws IllegalArgumentException if size is less than zero or size is greater than Integer.MAX_VALUE
     * @see IOUtils#toByteArray(java.io.InputStream, int)
     * @since 2.1
     */
    public static byte[] toByteArray( InputStream input, long size ) throws IOException 
    {

      if ( size > Integer.MAX_VALUE ) 
      {
          throw new IllegalArgumentException( I18n.err( I18n.ERR_17072_INTEGER_OVERFLOW, size ) );
      }

      return toByteArray( input, ( int ) size );
    }
    
    
    /**
     * Gets the contents of an <code>InputStream</code> as a list of Strings,
     * one entry per line, using the specified character encoding.
     * <p>
     * This method buffers the input internally, so there is no need to use a
     * <code>BufferedInputStream</code>.
     *
     * @param input  the <code>InputStream</code> to read from, not null
     * @param encoding  the encoding to use, null means platform default
     * @return the list of Strings, never null
     * @throws NullPointerException if the input is null
     * @throws IOException if an I/O error occurs
     * @since 2.3
     */
    public static List<String> readLines( InputStream input, Charset encoding ) throws IOException 
    {
        InputStreamReader reader = new InputStreamReader( input, toCharset( encoding ) );
        
        return readLines( reader );
    }
    
    
    /**
     * Gets the contents of a <code>Reader</code> as a list of Strings,
     * one entry per line.
     * <p>
     * This method buffers the input internally, so there is no need to use a
     * <code>BufferedReader</code>.
     *
     * @param input  the <code>Reader</code> to read from, not null
     * @return the list of Strings, never null
     * @throws NullPointerException if the input is null
     * @throws IOException if an I/O error occurs
     * @since 1.1
     */
    public static List<String> readLines( Reader input ) throws IOException 
    {
        BufferedReader reader = toBufferedReader( input );
        List<String> list = new ArrayList<>();
        String line = reader.readLine();
        
        while ( line != null ) 
        {
            list.add( line );
            line = reader.readLine();
        }
        
        return list;
    }

    
    /**
     * Returns the given reader if it is a {@link BufferedReader}, otherwise creates a BufferedReader from the given
     * reader.
     *
     * @param reader
     *            the reader to wrap or return (not null)
     * @return the given reader or a new {@link BufferedReader} for the given reader
     * @since 2.2
     * @throws NullPointerException if the input parameter is null
     */
    public static BufferedReader toBufferedReader( Reader reader ) 
    {
        return reader instanceof BufferedReader ? ( BufferedReader ) reader : new BufferedReader( reader );
    }
}
