/*******************************************************************************
 * Copyright (c) 2007 Bruno Medeiros and other Contributors.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Bruno Medeiros - initial implementation
 *******************************************************************************/
package melnorme.utilbox.misc;

import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.CharArrayWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.Charset;

/**
 * Miscellaneous stream utilities. 
 */
public class StreamUtil {
	
	public static final int EOF = -1;
	
	private static IOException createFailedToReadExpected(int length, int totalRead) {
		return new IOException("Failed to read requested amount of characters. " +
				"Read: " + totalRead + " of total requested: " + length);
	}
	
	
	/** Reads and returns all bytes from given inputStream until an EOF is read. 
	 * Closes inputStream afterwards. */
	public static IByteSequence readAllBytesFromStream(InputStream inputStream) throws IOException {
		return readAllBytesFromStream(inputStream, 32);
	}
	
	/** Reads and returns all bytes from given inputStream until an EOF is read. 
	 * Closes inputStream afterwards. 
	 * ByteArray buffer is initialized to given capacity. */
	public static IByteSequence readAllBytesFromStream(InputStream inputStream, int capacity) 
			throws IOException {
		try {
			final int BUFFER_SIZE = 1024;
			byte[] buffer = new byte[BUFFER_SIZE];
			ByteArrayOutputStreamExt bytes = new ByteArrayOutputStreamExt(capacity);
			
			int read;
			while((read = inputStream.read(buffer)) != EOF) {
				bytes.write(buffer, 0, read);
			}
			return bytes;
		} finally {
			inputStream.close();
		}
	}
	
	/** Reads and returns all chars from given reader until an EOF is read.
	 * Closes reader afterwards. */
	public static CharArrayWriter readAllCharsFromReader(Reader reader) throws IOException {
		try {
			final int BUFFER_SIZE = 1024;
			char[] buffer = new char[BUFFER_SIZE];
			CharArrayWriter chars = new CharArrayWriter();
			
			int read;
			while((read = reader.read(buffer)) != EOF) {
				chars.write(buffer, 0, read);
			}
			return chars;
		} finally {
			reader.close();
		}
	}
	
	/** Reads all chars from given reader until an EOF is read, and returns them as a String.
	 * Closes reader afterwards. */
	public static String readStringFromReader(Reader reader) throws IOException {
		return readAllCharsFromReader(reader).toString();
	}
	
	/** Reads given length amount of chars from given reader, and returns them in a char[]. 
	 *  Throws IOException if it fails to read given length amount of elements.
	 *  Closes reader afterwards. 
	 */
	public static char[] readRequiredAmountFromReader(Reader reader, int length) throws IOException {
		char[] chars = readCharAmountFromReader(reader, length);
		if(chars.length != length) {
			throw createFailedToReadExpected(length, chars.length);
		}
		return chars;
	}
	
	/** Attempts to read given length amount of chars from given reader, and returns them in a char[]. 
	 *  Closes reader afterwards. */
	public static char[] readCharAmountFromReader(Reader reader, int length) throws IOException 
	{
		try {
			char[] chars = new char[length];
			int totalRead = 0;
			do {
				int read = reader.read(chars, totalRead, length - totalRead);
				if (read == -1) {
					break;
				}
				totalRead += read;
			} while (totalRead != length);
			return chars;
		} finally {
			reader.close(); 
		}
	}
	
	/** Writes given bytes array to given outputStream. 
	 * Closes outputStream afterwards. */
	public static void writeBytesToStream(byte[] bytes, OutputStream outputStream) throws IOException {
		// A BufferedOutputStream is likely not necessary since this is a one-time array write
		BufferedOutputStream bos = new BufferedOutputStream(outputStream);
		try {
			bos.write(bytes);
		} finally {
			bos.close();
		}
	}
	
	/** Writes given chars array to given writer. 
	 * Close writer afterwards. */
	public static void writeCharsToWriter(char[] chars, Writer writer) throws IOException {
		BufferedWriter bw = new BufferedWriter(writer);
		try {
			bw.write(chars);
		} finally {
			bw.close();
		}
	}
	
	/** Writes given string to given writer. 
	 * Close writer afterwards. */
	public static void writeStringToWriter(String string, Writer writer) throws IOException {
		BufferedWriter bw = new BufferedWriter(writer);
		try {
			bw.write(string);
		} finally {
			bw.close();
		}
	}
	
	/** Writes given string to given stream, using given charset */
	public static void writeStringToStream(String string, OutputStream stream, Charset charset) throws IOException {
		OutputStreamWriter osw = new OutputStreamWriter(stream, charset);
		try {
			osw.append(string);
			osw.flush();
		} finally {
			osw.close();
		}
	}
	
	/** Copies given length amount of bytes from given inputStream to given outputStream. 
	 * Alternatively, if length == -1, copy all bytes in inputStream until EOF.
	 * @throws IOException if it fails to read given length amount of elements. */
	public static void copyBytesToStream(InputStream inputStream, OutputStream outputStream, int length)
			throws IOException {
		final int BUFFER_SIZE = 1024;
		byte[] buffer = new byte[BUFFER_SIZE];
		
		int totalRead = 0;
		do {
			int readReqLen = (length == -1) ? BUFFER_SIZE : Math.min(BUFFER_SIZE, length - totalRead);
			int read = inputStream.read(buffer, 0, readReqLen);
			if (read == -1) {
				if (length != -1) {
					throw createFailedToReadExpected(length, totalRead);
				} else {
					return; // Nothing more to copy.
				}
			}
			totalRead += read;
			outputStream.write(buffer, 0, read);
		} while (totalRead != length);
	}

	/** Copies all bytes in given inputStream (until EOF) to given outputStream. 
	 * Closes inputStream and outputStream. */
	public static void copyStream(InputStream inputStream, OutputStream outputStream) throws IOException {
		copyStream(inputStream, outputStream, true);
	}
	
	/** Copies all bytes in given inputStream (until EOF) to given outputStream. 
	 * Closes inputStream and also outputStream if given closeOut is true. */
	public static void copyStream(InputStream inputStream, OutputStream outputStream, boolean closeOut) 
			throws IOException {
		try {
			copyBytesToStream(inputStream, outputStream, -1);
		} finally {
			try {
				inputStream.close();
			} finally {
				if(closeOut) {
					outputStream.close();
				}
			}
		}
	}
	
	
	/** Closes given inputStream, either ignoring IOExceptions, 
	 * or rethrowing them unchecked, according to given rethrowAsUnchecked. */
	public static void uncheckedClose(InputStream inStream, boolean rethrowAsUnchecked) {
		try {
			inStream.close();
		} catch (IOException e) {
			if(rethrowAsUnchecked)
				throw melnorme.utilbox.core.ExceptionAdapter.unchecked(e);
		}
	}
	
	/** Closes given reader, either ignoring IOExceptions, 
	 * or rethrowing them unchecked, according to given rethrowAsUnchecked. */
	public static void uncheckedClose(Reader reader, boolean rethrowAsUnchecked) {
		try {
			reader.close();
		} catch (IOException e) {
			if(rethrowAsUnchecked)
				throw melnorme.utilbox.core.ExceptionAdapter.unchecked(e);
		}
	}
	
}
