package org.andengine.util;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
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.StringWriter;
import java.io.Writer;
import java.nio.ByteBuffer;
import java.util.ArrayList;

import org.andengine.util.debug.Debug;

/**
 * (c) 2010 Nicolas Gramlich
 * (c) 2011 Zynga Inc.
 *
 * @author Nicolas Gramlich
 * @since 15:48:56 - 03.09.2009
 */
public final class StreamUtils {
	// ===========================================================
	// Constants
	// ===========================================================

	public static final int IO_BUFFER_SIZE = 8 * 1024;

	private static final int END_OF_STREAM = -1;

	// ===========================================================
	// Fields
	// ===========================================================

	// ===========================================================
	// Constructors
	// ===========================================================

	private StreamUtils() {

	}

	// ===========================================================
	// Getter & Setter
	// ===========================================================

	// ===========================================================
	// Methods from SuperClass/Interfaces
	// ===========================================================

	// ===========================================================
	// Methods
	// ===========================================================

	public static String[] readLines(final InputStream pInputStream) throws IOException {
		return StreamUtils.readLines(new InputStreamReader(pInputStream));
	}

	public static String[] readLines(final Reader pReader) throws IOException {
		final BufferedReader reader = new BufferedReader(pReader);

		final ArrayList<String> lines = new ArrayList<String>();

		String line = null;
		while ((line = reader.readLine()) != null) {
			lines.add(line);
		}

		return lines.toArray(new String[lines.size()]);
	}

	public static final String readFully(final InputStream pInputStream) throws IOException {
		final StringWriter writer = new StringWriter();
		final char[] buf = new char[StreamUtils.IO_BUFFER_SIZE];
		try {
			final Reader reader = new BufferedReader(new InputStreamReader(pInputStream, "UTF-8"));
			int read;
			while ((read = reader.read(buf)) != StreamUtils.END_OF_STREAM) {
				writer.write(buf, 0, read);
			}
		} finally {
			StreamUtils.close(pInputStream);
		}
		return writer.toString();
	}

	public static final byte[] streamToBytes(final InputStream pInputStream) throws IOException {
		return StreamUtils.streamToBytes(pInputStream, StreamUtils.END_OF_STREAM);
	}

	public static final byte[] streamToBytes(final InputStream pInputStream, final int pReadLimit) throws IOException {
		final ByteArrayOutputStream os = new ByteArrayOutputStream((pReadLimit == StreamUtils.END_OF_STREAM) ? StreamUtils.IO_BUFFER_SIZE : pReadLimit);
		StreamUtils.copy(pInputStream, os, pReadLimit);
		return os.toByteArray();
	}

	/**
	 * @see {@link #streamToBytes(InputStream, int, byte[], int)}
	 */
	public static final void streamToBytes(final InputStream pInputStream, final int pByteLimit, final byte[] pData) throws IOException {
		StreamUtils.streamToBytes(pInputStream, pByteLimit, pData, 0);
	}

	/**
	 * @param pInputStream the sources of the bytes.
	 * @param pByteLimit the amount of bytes to read.
	 * @param pData the array to place the read bytes in.
	 * @param pOffset the offset within pData.
	 * @throws IOException
	 */
	public static final void streamToBytes(final InputStream pInputStream, final int pByteLimit, final byte[] pData, final int pOffset) throws IOException {
		if (pByteLimit > pData.length - pOffset) {
			throw new IOException("pData is not big enough.");
		}

		int pBytesLeftToRead = pByteLimit;
		int readTotal = 0;
		int read;
		while ((read = pInputStream.read(pData, pOffset + readTotal, pBytesLeftToRead)) != StreamUtils.END_OF_STREAM) {
			readTotal += read;
			if (pBytesLeftToRead > read) {
				pBytesLeftToRead -= read;
			} else {
				break;
			}
		}

		if (readTotal != pByteLimit) {
			throw new IOException("ReadLimit: '" + pByteLimit + "', Read: '" + readTotal + "'.");
		}
	}

	public static final void copy(final InputStream pInputStream, final OutputStream pOutputStream) throws IOException {
		StreamUtils.copy(pInputStream, pOutputStream, StreamUtils.END_OF_STREAM);
	}

	public static final void copy(final InputStream pInputStream, final byte[] pData) throws IOException {
		int dataOffset = 0;
		final byte[] buf = new byte[StreamUtils.IO_BUFFER_SIZE];
		int read;
		while ((read = pInputStream.read(buf)) != StreamUtils.END_OF_STREAM) {
			System.arraycopy(buf, 0, pData, dataOffset, read);
			dataOffset += read;
		}
	}

	public static final void copy(final InputStream pInputStream, final ByteBuffer pByteBuffer) throws IOException {
		final byte[] buf = new byte[StreamUtils.IO_BUFFER_SIZE];
		int read;
		while ((read = pInputStream.read(buf)) != StreamUtils.END_OF_STREAM) {
			pByteBuffer.put(buf, 0, read);
		}
	}

	/**
	 * Copy the content of the input stream into the output stream, using a temporary
	 * byte array buffer whose size is defined by {@link #IO_BUFFER_SIZE}.
	 *
	 * @param pInputStream The input stream to copy from.
	 * @param pOutputStream The output stream to copy to.
	 * @param pByteLimit not more than so much bytes to read, or unlimited if {@link #END_OF_STREAM}.
	 *
	 * @throws IOException If any error occurs during the copy.
	 */
	public static final void copy(final InputStream pInputStream, final OutputStream pOutputStream, final int pByteLimit) throws IOException {
		if (pByteLimit == StreamUtils.END_OF_STREAM) {
			final byte[] buf = new byte[StreamUtils.IO_BUFFER_SIZE];
			int read;
			while ((read = pInputStream.read(buf)) != StreamUtils.END_OF_STREAM) {
				pOutputStream.write(buf, 0, read);
			}
		} else {
			final byte[] buf = new byte[StreamUtils.IO_BUFFER_SIZE];
			final int bufferReadLimit = Math.min((int) pByteLimit, StreamUtils.IO_BUFFER_SIZE);
			long pBytesLeftToRead = pByteLimit;

			int read;
			while ((read = pInputStream.read(buf, 0, bufferReadLimit)) != StreamUtils.END_OF_STREAM) {
				if (pBytesLeftToRead > read) {
					pOutputStream.write(buf, 0, read);
					pBytesLeftToRead -= read;
				} else {
					pOutputStream.write(buf, 0, (int) pBytesLeftToRead);
					break;
				}
			}
		}
		pOutputStream.flush();
	}

	public static final boolean copyAndClose(final InputStream pInputStream, final OutputStream pOutputStream) {
		try {
			StreamUtils.copy(pInputStream, pOutputStream, StreamUtils.END_OF_STREAM);
			return true;
		} catch (final IOException e) {
			return false;
		} finally {
			StreamUtils.close(pInputStream);
			StreamUtils.close(pOutputStream);
		}
	}

	public static final void close(final Closeable pCloseable) {
		if (pCloseable != null) {
			try {
				pCloseable.close();
			} catch (final IOException e) {
				Debug.e("Error closing Closable", e);
			}
		}
	}

	public static final void flushAndCloseStream(final OutputStream pOutputStream) {
		if (pOutputStream != null) {
			try {
				pOutputStream.flush();
			} catch (final IOException e) {
				Debug.e("Error flusing OutputStream", e);
			} finally {
				StreamUtils.close(pOutputStream);
			}
		}
	}

	public static final void flushAndCloseWriter(final Writer pWriter) {
		if (pWriter != null) {
			try {
				pWriter.flush();
			} catch (final IOException e) {
				Debug.e("Error flusing Writer", e);
			} finally {
				StreamUtils.close(pWriter);
			}
		}
	}

	// ===========================================================
	// Inner and Anonymous Classes
	// ===========================================================
}