// ***************************************************************************************************************************
// * 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                                                              *
// *                                                                                                                         *
// *  http://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.juneau.internal;

import static org.apache.juneau.internal.ThrowableUtils.*;
import static org.apache.juneau.internal.StringUtils.*;

import java.io.*;
import java.nio.charset.*;

import org.apache.juneau.*;
import org.apache.juneau.utils.*;

/**
 * Various I/O related utility methods.
 */
public final class IOUtils {

	/** UTF-8 charset */
	public static final Charset UTF8 = Charset.forName("UTF-8");

	/**
	 * Reads the contents of a file into a string.
	 *
	 * @param path The path of the file to read using default character encoding.
	 * @return The contents of the reader as a string, or <jk>null</jk> if file does not exist.
	 * @throws IOException If a problem occurred trying to read from the reader.
	 */
	public static String readFile(String path) throws IOException {
		return read(new File(path));
	}

	/**
	 * Reads the specified object to a <c>String</c>.
	 *
	 * <p>
	 * Can be any of the following object types:
	 * <ul>
	 * 	<li>{@link CharSequence}
	 * 	<li>{@link File}
	 * 	<li>{@link Reader}
	 * 	<li>{@link InputStream}
	 * 	<li><code><jk>byte</jk>[]</code>
	 * </ul>
	 *
	 * @param in The object to read.
	 * @return The object serialized to a string, or <jk>null</jk> if it wasn't a supported type.
	 * @throws IOException Thrown by underlying stream.
	 */
	public static String read(Object in) throws IOException {
		if (in == null)
			return null;
		if (in instanceof CharSequence)
			return in.toString();
		if (in instanceof File)
			return read((File)in);
		if (in instanceof Reader)
			return read((Reader)in);
		if (in instanceof InputStream)
			return read((InputStream)in);
		if (in instanceof byte[])
			return read(new ByteArrayInputStream((byte[])in));
		throw new IOException("Cannot convert object of type '"+in.getClass().getName()+"' to a String.");
	}

	/**
	 * Same as {@link #read(Object)} but appends all the input into a single String.
	 *
	 * @param in The objects to read.
	 * @return The objects serialized to a string, never <jk>null</jk>.
	 * @throws IOException Thrown by underlying stream.
	 */
	public static String readAll(Object...in) throws IOException {
		if (in.length == 1)
			return read(in[0]);
		StringWriter sw = new StringWriter();
		for (Object o : in)
			sw.write(emptyIfNull(read(o)));
		return sw.toString();
	}

	/**
	 * Reads the contents of a file into a string.
	 *
	 * @param in The file to read using default character encoding.
	 * @return The contents of the reader as a string, or <jk>null</jk> if file does not exist.
	 * @throws IOException If a problem occurred trying to read from the reader.
	 */
	public static String read(File in) throws IOException {
		if (in == null || ! in.exists())
			return null;
		try (Reader r = FileReaderBuilder.create(in).build()) {
			return read(r, 0, 1024);
		}
	}

	/**
	 * Reads the contents of a reader into a string.
	 *
	 * @param in The input reader.
	 * @return The contents of the reader as a string.
	 * @throws IOException If a problem occurred trying to read from the reader.
	 */
	public static String read(Reader in) throws IOException {
		return read(in, 0, 1024);
	}

	/**
	 * Reads the contents of an input stream into a string using the specified charset.
	 *
	 * @param in The input stream.
	 * @param cs The charset of the contents of the input stream.
	 * @return The contents of the reader as a string.  <jk>null</jk> if input stream was null.
	 * @throws IOException If a problem occurred trying to read from the input stream.
	 */
	public static String read(InputStream in, Charset cs) throws IOException {
		if (in == null)
			return null;
		return read(new InputStreamReader(in, cs));
	}

	/**
	 * Reads the contents of an input stream into a string using the system default charset.
	 *
	 * @param in The input stream.
	 * @return The contents of the reader as a string, or <jk>null</jk> if the input stream is null.
	 * @throws IOException If a problem occurred trying to read from the input stream.
	 */
	public static String read(InputStream in) throws IOException {
		if (in == null)
			return null;
		return read(new InputStreamReader(in, Charset.defaultCharset()));
	}

	/**
	 * Reads the specified input into a {@link String} until the end of the input is reached.
	 *
	 * <p>
	 * The {@code Reader} is automatically closed.
	 *
	 * <p>
	 * If the {@code Reader} is not an instance of a {@code BufferedReader}, then it gets wrapped in a
	 * {@code BufferedReader}.
	 *
	 * @param in The input reader.
	 * @param length Specify a positive number if the length of the input is known.
	 * @param bufferSize Specify the buffer size to use.
	 * @return The contents of the reader as a string.  <jk>null</jk> if reader was null.
	 * @throws IOException If a problem occurred trying to read from the reader.
	 */
	public static String read(Reader in, int length, int bufferSize) throws IOException {
		if (in == null)
			return null;
		if (bufferSize == 0)
			bufferSize = 1024;
		length = (length <= 0 ? bufferSize : length);
		StringBuilder sb = new StringBuilder(length); // Assume they're ASCII characters.
		try {
			char[] buf = new char[Math.min(bufferSize, length)];
			int i = 0;
			while ((i = in.read(buf)) != -1)
				sb.append(buf, 0, i);
			return sb.toString();
		} finally {
			in.close();
		}
	}

	/**
	 * Read the specified object into a byte array.
	 *
	 * @param in
	 * 	The object to read into a byte array.
	 * 	<br>Can be any of the following types:
	 * 	<ul>
	 * 		<li><code><jk>byte</jk>[]</code>
	 * 		<li>{@link InputStream}
	 * 		<li>{@link Reader}
	 * 		<li>{@link CharSequence}
	 * 		<li>{@link File}
	 * 	</ul>
	 * @param buffSize
	 * 	The buffer size to use.
	 * @return The contents of the stream as a byte array.
	 * @throws IOException Thrown by underlying stream or if object is not a supported type.
	 */
	public static byte[] readBytes(Object in, int buffSize) throws IOException {
		if (in == null)
			return new byte[0];
		if (in instanceof byte[])
			return (byte[])in;
		if (in instanceof CharSequence)
			return in.toString().getBytes(UTF8);
		if (in instanceof InputStream)
			return readBytes((InputStream)in, buffSize);
		if (in instanceof Reader)
			return read((Reader)in, 0, buffSize).getBytes(UTF8);
		if (in instanceof File)
			return readBytes((File)in, buffSize);
		throw new IOException("Cannot convert object of type '"+in.getClass().getName()+"' to a byte array.");
	}

	/**
	 * Read the specified input stream into a byte array.
	 *
	 * @param in
	 * 	The stream to read into a byte array.
	 * @return The contents of the stream as a byte array.
	 * @throws IOException Thrown by underlying stream.
	 */
	public static byte[] readBytes(InputStream in) throws IOException {
		return in == null ? null : readBytes(in, 1024);
	}

	/**
	 * Read the specified input stream into a byte array.
	 *
	 * @param in
	 * 	The stream to read into a byte array.
	 * @param buffSize
	 * 	The buffer size to use.
	 * @return The contents of the stream as a byte array.
	 * @throws IOException Thrown by underlying stream.
	 */
	public static byte[] readBytes(InputStream in, int buffSize) throws IOException {
		if (buffSize == 0)
			buffSize = 1024;
        try (final ByteArrayOutputStream buff = new ByteArrayOutputStream(buffSize)) {
			int nRead;
			byte[] b = new byte[buffSize];
			while ((nRead = in.read(b, 0, b.length)) != -1)
				buff.write(b, 0, nRead);
			buff.flush();
			return buff.toByteArray();
        }
	}

	/**
	 * Read the specified file into a byte array.
	 *
	 * @param in
	 * 	The file to read into a byte array.
	 * @return The contents of the file as a byte array.
	 * @throws IOException Thrown by underlying stream.
	 */
	public static byte[] readBytes(File in) throws IOException {
		return readBytes(in, 1024);
	}

	/**
	 * Read the specified file into a byte array.
	 *
	 * @param in
	 * 	The file to read into a byte array.
	 * @param buffSize
	 * 	The buffer size to use.
	 * @return The contents of the file as a byte array.
	 * @throws IOException Thrown by underlying stream.
	 */
	public static byte[] readBytes(File in, int buffSize) throws IOException {
		if (buffSize == 0)
			buffSize = 1024;
		if (! (in.exists() && in.canRead()))
			return new byte[0];
		buffSize = Math.min((int)in.length(), buffSize);
		try (FileInputStream fis = new FileInputStream(in)) {
			return readBytes(fis, buffSize);
		}
	}

	/**
	 * Shortcut for calling <c>readBytes(in, 1024);</c>
	 *
	 * @param in
	 * 	The object to read into a byte array.
	 * 	<br>Can be any of the following types:
	 * 	<ul>
	 * 		<li><code><jk>byte</jk>[]</code>
	 * 		<li>{@link InputStream}
	 * 		<li>{@link Reader}
	 * 		<li>{@link CharSequence}
	 * 		<li>{@link File}
	 * 	</ul>
	 * @return The contents of the stream as a byte array.
	 * @throws IOException Thrown by underlying stream or if object is not a supported type.
	 */
	public static byte[] readBytes(Object in) throws IOException {
		return readBytes(in, 1024);
	}

	/**
	 * Same as {@link #readBytes(Object)} but appends all the input into a single byte array.
	 *
	 * @param in The objects to read.
	 * @return The objects serialized to a byte array, never <jk>null</jk>.
	 * @throws IOException Thrown by underlying stream.
	 */
	public static byte[] readBytes(Object...in) throws IOException {
		if (in.length == 1)
			return readBytes(in[0]);
        try (final ByteArrayOutputStream buff = new ByteArrayOutputStream(1024)) {
			for (Object o : in) {
				byte[] bo = readBytes(o);
				if (bo != null)
					buff.write(bo);
			}
			buff.flush();
			return buff.toByteArray();
        }
	}

	/**
	 * Writes the contents of the specified <c>Reader</c> to the specified file.
	 *
	 * @param out The file to write the output to.
	 * @param in The reader to pipe from.
	 * @return The number of characters written to the file.
	 * @throws IOException Thrown by underlying stream.
	 */
	public static int write(File out, Reader in) throws IOException {
		assertFieldNotNull(out, "out");
		assertFieldNotNull(in, "in");
		try (Writer w = FileWriterBuilder.create(out).build()) {
			return IOPipe.create(in, w).run();
		}
	}

	/**
	 * Writes the contents of the specified <c>InputStream</c> to the specified file.
	 *
	 * @param out The file to write the output to.
	 * @param in The input stream to pipe from.
	 * @return The number of characters written to the file.
	 * @throws IOException Thrown by underlying stream.
	 */
	public static int write(File out, InputStream in) throws IOException {
		assertFieldNotNull(out, "out");
		assertFieldNotNull(in, "in");
		try (OutputStream os = new FileOutputStream(out)) {
			return IOPipe.create(in, os).run();
		}
	}

	/**
	 * Pipes the contents of the specified object into the writer.
	 *
	 * <p>
	 * The reader is closed, the writer is not.
	 *
	 * @param in
	 * 	The input to pipe from.
	 * 	Can be any of the types defined by {@link #toReader(Object)}.
	 * @param out
	 * 	The writer to pipe to.
	 * @throws IOException Thrown by underlying stream.
	 */
	public static void pipe(Object in, Writer out) throws IOException {
		IOPipe.create(in, out).run();
	}

	/**
	 * Pipes the contents of the specified object into the output stream.
	 *
	 * <p>
	 * The input stream is closed, the output stream is not.
	 *
	 * @param in
	 * 	The input to pipe from.
	 * 	Can be any of the types defined by {@link #toInputStream(Object)}.
	 * @param out
	 * 	The writer to pipe to.
	 * @throws IOException Thrown by underlying stream.
	 */
	public static void pipe(Object in, OutputStream out) throws IOException {
		IOPipe.create(in, out).run();
	}

	/**
	 * Wraps the specified reader in a buffered reader.
	 *
	 * @param r The reader being wrapped.
	 * @return
	 * 	The reader wrapped in a {@link BufferedReader}, or the original {@link Reader} if it's already a buffered
	 * 	reader.
	 */
	public static Reader getBufferedReader(Reader r) {
		if (r == null || r instanceof BufferedReader || r instanceof StringReader)
			return r;
		return new BufferedReader(r);
	}

	/**
	 * Counts the number of bytes in the input stream and then closes the stream.
	 *
	 * @param is The input stream to read from.
	 * @return The number of bytes read.
	 * @throws IOException Thrown by underlying stream.
	 */
	public static long count(InputStream is) throws IOException {
		assertFieldNotNull(is, "is");
		long c = 0;
		long i;
		try {
			while ((i = is.skip(1024)) != 0)
				c += i;
		} finally {
			is.close();
		}
		return c;
	}

	/**
	 * Counts the number of characters in the reader and then closes the reader.
	 *
	 * @param r The reader to read from.
	 * @return The number of characters read.
	 * @throws IOException Thrown by underlying stream.
	 */
	public static long count(Reader r) throws IOException {
		assertFieldNotNull(r, "r");
		long c = 0;
		long i;
		try {
			while ((i = r.skip(1024)) != 0)
				c += i;
		} finally {
			r.close();
		}
		return c;
	}

	/**
	 * Given the specified <js>"Content-Length"</js> header value, return an appropriate buffer size.
	 *
	 * <p>
	 * The maximum buffer size is 1MB.
	 *
	 * @param contentLength The value of the <js>"Content-Length"</js> header.
	 * @return The appropriate buffer size.
	 */
	public static int getBufferSize(String contentLength) {
		try {
			if (isNotEmpty(contentLength)) {
				long l = Long.decode(contentLength);
				if (l > 1048576)
					return 1048576;
				if (l <= 0)
					return 8192;
				return (int)l;
			}
		} catch (Exception e) {
			return 8192;
		}
		return 8192;
	}

	/**
	 * Close input stream and ignore any exceptions.
	 *
	 * <p>
	 * No-op if input stream is <jk>null</jk>.
	 *
	 * @param is The input stream to close.
	 */
	public static void closeQuietly(InputStream is) {
		try {
			if (is != null)
				is.close();
		} catch (IOException e) {}
	}

	/**
	 * Close output stream and ignore any exceptions.
	 *
	 * <p>
	 * No-op if output stream is <jk>null</jk>.
	 *
	 * @param os The output stream to close.
	 */
	public static void closeQuietly(OutputStream os) {
		try {
			if (os != null)
				os.close();
		} catch (IOException e) {}
	}

	/**
	 * Close reader and ignore any exceptions.
	 *
	 * <p>
	 * No-op if reader is <jk>null</jk>.
	 *
	 * @param r The reader to close.
	 */
	public static void closeQuietly(Reader r) {
		try {
			if (r != null)
				r.close();
		} catch (IOException e) {}
	}

	/**
	 * Close writer and ignore any exceptions.
	 *
	 * <p>
	 * No-op if writer is <jk>null</jk>.
	 *
	 * @param w The writer to close.
	 */
	public static void closeQuietly(Writer w) {
		try {
			if (w != null)
				w.close();
		} catch (IOException e) {}
	}

	/**
	 * Quietly close all specified input streams, output streams, readers, and writers.
	 *
	 * @param o The list of all objects to quietly close.
	 */
	public static void closeQuietly(Object...o) {
		for (Object o2 : o) {
			if (o2 instanceof InputStream)
				closeQuietly((InputStream)o2);
			if (o2 instanceof OutputStream)
				closeQuietly((OutputStream)o2);
			if (o2 instanceof Reader)
				closeQuietly((Reader)o2);
			if (o2 instanceof Writer)
				closeQuietly((Writer)o2);
		}
	}

	/**
	 * Flushes multiple output streams and writers in a single call.
	 *
	 * @param o
	 * 	The objects to flush.
	 * 	<jk>null</jk> entries are ignored.
	 * @throws IOException Thrown by underlying stream.
	 */
	public static void flush(Object...o) throws IOException {
		IOException ex = null;
		for (Object o2 : o) {
			try {
				if (o2 instanceof OutputStream)
					((OutputStream)o2).flush();
				if (o2 instanceof Writer)
					((Writer)o2).flush();
			} catch (IOException e) {
				ex = e;
			}
		}
		if (ex != null)
			throw ex;
	}

	/**
	 * Close all specified input streams, output streams, readers, and writers.
	 *
	 * @param o
	 * 	The list of all objects to close.
	 * 	<jk>null</jk> entries are ignored.
	 * @throws IOException Thrown by underlying stream.
	 */
	public static void close(Object...o) throws IOException {
		IOException ex = null;
		for (Object o2 : o) {
			try {
				if (o2 instanceof InputStream)
					((InputStream)o2).close();
				if (o2 instanceof OutputStream)
					((OutputStream)o2).close();
				if (o2 instanceof Reader)
					((Reader)o2).close();
				if (o2 instanceof Writer)
					((Writer)o2).close();
			} catch (IOException e) {
				ex = e;
			}
		}
		if (ex != null)
			throw ex;
	}

	/**
	 * Converts an object to a <c>Reader</c>.
	 *
	 * @param o
	 * 	The object to convert to a reader.
	 * 	Can be any of the following:
	 * 	<ul>
	 * 		<li>{@link InputStream}
	 * 		<li>{@link Reader}
	 * 		<li>{@link File}
	 * 		<li>{@link CharSequence}
	 * 		<li><code><jk>byte</jk>[]</code>
	 * 		<li><code><jk>null</jk></code> - Returns <jk>null</jk>.
	 * 	</ul>
	 * @return The object converted to a reader.
	 * @throws IOException If file could not be read.
	 * @throws IllegalArgumentException If invalid object passed in.
	 */
	public static Reader toReader(Object o) throws IOException {
		if (o == null)
			return null;
		if (o instanceof CharSequence)
			return new StringReader(o.toString());
		if (o instanceof File)
			return new FileReader((File)o);
		if (o instanceof Reader)
			return (Reader)o;
		if (o instanceof InputStream)
			return new InputStreamReader((InputStream)o, "UTF-8");
		if (o instanceof byte[])
			return new InputStreamReader(new ByteArrayInputStream((byte[])o), "UTF-8");
		throw new FormattedIllegalArgumentException("Invalid object of type {0} passed to IOUtils.toReader(Object)", o.getClass());
	}

	/**
	 * Converts an object to an <c>InputStream</c>.
	 *
	 * @param o
	 * 	The object to convert to an input stream.
	 * 	Can be any of the following:
	 * 	<ul>
	 * 		<li>{@link InputStream}
	 * 		<li>{@link Reader}
	 * 		<li>{@link File}
	 * 		<li>{@link CharSequence} - Converted to UTF-8 stream.
	 * 		<li><code><jk>byte</jk>[]</code>
	 * 		<li><code><jk>null</jk></code> - Returns <jk>null</jk>.
	 * 	</ul>
	 * @return The object converted to an input stream.
	 * @throws IOException If file could not be read.
	 * @throws IllegalArgumentException If invalid object passed in.
	 */
	public static InputStream toInputStream(Object o) throws IOException {
		if (o == null)
			return null;
		if (o instanceof InputStream)
			return (InputStream)o;
		if (o instanceof File)
			return new FileInputStream((File)o);
		if (o instanceof byte[])
			return new ByteArrayInputStream((byte[])o);
		if (o instanceof CharSequence)
			return new ByteArrayInputStream(((CharSequence)o).toString().getBytes(UTF8));
		if (o instanceof Reader)
			return new ByteArrayInputStream(IOUtils.read((Reader)o).getBytes(UTF8));
		throw new FormattedIllegalArgumentException("Invalid object of type {0} passed to IOUtils.toInputStream(Object)", o.getClass());
	}

	/**
	 * Writes the specified string to the specified file.
	 *
	 * @param path The file path.
	 * @param contents The new file contents.
	 * @throws IOException Thrown by underlying stream.
	 */
	public static void write(String path, String contents) throws IOException {
		write(new File(path), new StringReader(contents));
	}

	/**
	 * Loads a text file from either the file system or classpath.
	 *
	 * @param name The file name.
	 * @param paths The paths to search.
	 * @return The file contents, or <jk>null</jk> if not found.
	 * @throws IOException Thrown by underlying stream.
	 */
	public static String loadSystemResourceAsString(String name, String...paths) throws IOException {
		for (String path : paths) {
			File p = new File(path);
			if (p.exists()) {
	 			File f = new File(p, name);
	 			if (f.exists() && f.canRead())
	 				return read(f);
			}
		}
		ClassLoader cl = Thread.currentThread().getContextClassLoader();
		if (cl == null)
			cl = ClassLoader.getSystemClassLoader();
		for (String path : paths) {
			String n = ".".equals(path) ? name : path + '/' + name;
			try (InputStream is = cl.getResourceAsStream(n)) {
				if (is != null)
					return read(is);
			}
			try (InputStream is = ClassLoader.getSystemResourceAsStream(n)) {
				if (is != null)
					return read(is);
			}
		}
		return null;
	}
}
