/* RepeatableInputStream.java

 Purpose:
 
 Description:
 
 History:
 Mar 12, 2008 12:03:53 PM , Created by jumperchen

 Copyright (C) 2008 Potix Corporation. All Rights Reserved.

 {{IS_RIGHT
 This program is distributed under LGPL Version 2.1 in the hope that
 it will be useful, but WITHOUT ANY WARRANTY.
 }}IS_RIGHT
 */
package org.zkoss.io;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.net.URL;
import java.nio.file.Files;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zkoss.lang.Library;

/**
 * {@link RepeatableInputStream} adds functionality to another input stream,
 * the ability to read repeatedly.
 * By repeatable-read we mean, after {@link #close}, the next invocation of
 * {@link #read} will re-open the input stream.
 *
 * <p>{@link RepeatableInputStream} actually creates a temporary space
 * to buffer the content, so it can be re-opened again after closed.
 * Notice that the temporary space (a.k.a., the buffered input stream)
 * is never closed until garbage-collected.
 *
 * <p>If the content size of the given input stream is smaller than
 * the value specified in the system property called
 * "org.zkoss.io.memoryLimitSize", the content will be buffered in
 * the memory. If the size exceeds, the content will be buffered in
 * a temporary file. By default, it is 512KB.
 * Note: the maximal value is {@link Integer#MAX_VALUE}
 *
 * <p>If the content size of the given input stream is larger than
 * the value specified in the system property called
 * "org.zkoss.io.bufferLimitSize", the content won't be buffered,
 * and it means the read is not repeatable. By default, it is 20MB.
 * Note: the maximal value is {@link Integer#MAX_VALUE}
 * 
 * @author jumperchen
 * @author tomyeh
 * @since 3.0.4
 */
public class RepeatableInputStream extends InputStream implements Repeatable,
		Serializable {
	private static final Logger log = LoggerFactory.getLogger(RepeatableInputStream.class);

	/*package*/ static final String BUFFER_LIMIT_SIZE = "org.zkoss.io.bufferLimitSize";
	/*package*/ static final String MEMORY_LIMIT_SIZE = "org.zkoss.io.memoryLimitSize";

	private transient InputStream _org;
	private transient OutputStream _out;
	private transient InputStream _in;
	private transient File _f;
	/** The content size. It is meaningful only if !_nobuf.
	 * Note: int is enough (since long makes no sense for buffering)
	 */
	private int _cntsz;
	private final int _bufmaxsz, _memmaxsz;
	private boolean _nobuf;

	private RepeatableInputStream(InputStream is) {
		_org = is;
		_bufmaxsz = Library.getIntProperty(BUFFER_LIMIT_SIZE, 20 * 1024 * 1024);
		_memmaxsz = Library.getIntProperty(MEMORY_LIMIT_SIZE, 512 * 1024);
	}

	/**
	 * Returns an input stream that can be read repeatedly, or null if the
	 * given input stream is null.
	 * Note: the returned input stream encapsulates the given input stream, rd
	 * (a.k.a., the buffered input stream) to adds the functionality to
	 * re-opens the input stream once {@link #close} is called.
	 *
	 * <p>By repeatable-read we mean, after {@link #close}, the next
	 * invocation of {@link #read} will re-open the input stream.
	 *
	 * <p>Use this method instead of instantiating {@link RepeatableInputStream}
	 * with the constructor.
	 *
	 * @see #getInstance(File)
	 */
	public static InputStream getInstance(InputStream is) {
		if (is instanceof ByteArrayInputStream)
			return new ResetableInputStream(is);
		else if (is != null && !(is instanceof Repeatable))
			return new RepeatableInputStream(is);
		return is;
	}
	/**
	 * Returns an input stream of a file that can be read repeatedly.
	 * Note: it assumes the file is binary (rather than text).
	 *
	 * <p>By repeatable-read we mean, after {@link #close}, the next
	 * invocation of {@link #read} will re-open the input stream.
	 *
	 * <p>Note: it is efficient since we don't have to buffer the
	 * content of the file to make it repeatable-read.
	 *
	 * @exception IllegalArgumentException if file is null.
	 * @exception FileNotFoundException if file not found
	 * @see #getInstance(InputStream)
 	 * @see #getInstance(String)
	 */
	public static InputStream getInstance(File file)
	throws FileNotFoundException {
		if (file == null)
			throw new IllegalArgumentException("null");
		if (!file.exists())
			throw new FileNotFoundException(file.toString());
		return new RepeatableFileInputStream(file);
	}
	/**
	 * Returns an input stream of a file that can be read repeatedly.
	 * Note: it assumes the file is binary (rather than text).
	 *
	 * <p>By repeatable-read we mean, after {@link #close}, the next
	 * invocation of {@link #read} will re-open the input stream.
	 *
	 * <p>Note: it is efficient since we don't have to buffer the
	 * content of the file to make it repeatable-read.
	 *
	 * @exception IllegalArgumentException if file is null.
	 * @see #getInstance(InputStream)
	 * @see #getInstance(File)
	 */
	public static InputStream getInstance(String filename)
	throws FileNotFoundException {
		return getInstance(new File(filename));
	}
	/**
	 * Returns an input stream of the resource of the given URL
	 * that can be read repeatedly.
	 * Note: it assumes the resource is binary (rather than text).
	 *
	 * <p>By repeatable-read we mean, after {@link #close}, the next
	 * invocation of {@link #read} will re-open the input stream.
	 *
	 * <p>Note: it is efficient since we don't have to buffer the
	 * content of the resource to make it repeatable-read.
	 *
	 * @exception IllegalArgumentException if url is null.
	 * @see #getInstance(InputStream)
 	 * @see #getInstance(String)
	 */
	public static InputStream getInstance(URL url) {
		if (url == null)
			throw new IllegalArgumentException("null");
		return new RepeatableURLInputStream(url);
	}

	private OutputStream getOutputStream() {
		if (_out == null)
			return _nobuf ? null: (_out = new ByteArrayOutputStream());
				//it is possible _membufsz <= 0, but OK to use memory first

		if (_cntsz >= _bufmaxsz) { //too large to buffer
			disableBuffering();
			return null;
		}

		if (_f == null && _cntsz >= _memmaxsz) { //memory to file
			try {
				final File f =
					new File(System.getProperty("java.io.tmpdir"), "zk");
				if (!f.isDirectory())
					f.mkdir();
				_f = File.createTempFile("zk.io", ".zk.io", f);
				final byte[] bs = ((ByteArrayOutputStream)_out).toByteArray();
				_out = new BufferedOutputStream(new FileOutputStream(_f));
				_out.write(bs);
			} catch (Throwable ex) {
				log.warn("Ignored: failed to buffer to a file, {}\nCause: {}", _f, ex.getMessage());
				disableBuffering();
			}
		}
		return _out;
	}
	private void disableBuffering() {
		_nobuf = true;
		if (_out != null) {
			try {
				_out.close();
			} catch (Throwable ex) { //ignore
			}
			_out = null;
		}
		if (_f != null) {
			try {
				Files.delete(_f.toPath());
			} catch (Throwable ex) { //ignore
			}
			_f = null;
		}
	}

	public int read() throws IOException {
		if (_org != null) {
			final int b = _org.read();
			if (!_nobuf && (b >= 0)) {
				final OutputStream out = getOutputStream();
				if (out != null)
					out.write(b);
				++_cntsz;
			}
			return b;
		} else {
			if (_in == null)
				_in = new BufferedInputStream(new FileInputStream(_f)); //_f must be non-null

			return _in.read();
		}
	}

	@Override
	public int read(byte[] b, int off, int len) throws IOException {
		if (_org != null) {
			final int realLen = _org.read(b, off, len);
			if (!_nobuf)
				if (realLen >= 0) {
					final OutputStream out = getOutputStream();
					if (out != null) out.write(b, off, realLen);
					_cntsz += realLen;
				}
			return realLen;
		} else {
			if (_in == null)
				_in = new BufferedInputStream(new FileInputStream(_f)); //_f must be non-null

			return _in.read(b, off, len);
		}
	}

	/** Closes the current access, and the next call of {@link #close}
	 * re-opens the buffered input stream.
	 */
	public void close() throws IOException {
		_cntsz = 0;
		if (_org != null) {
			_org.close();

			if (_out != null) {
				try {
					_out.close();
				} catch (Throwable ex) {
					log.warn("Ignored: failed to close the buffer.\nCause: "+ex.getMessage());
					disableBuffering();
					return;
				}
				if (_f == null)
					_in = new ByteArrayInputStream(
						((ByteArrayOutputStream)_out).toByteArray());
					//we don't initialize _in if _f is not null
					//to reduce memory use (after all, read might not be called)
				_out = null;
				_org = null;
			}
		} else if (_in != null) {
			if (_f != null) {
				_in.close();
				_in = null;
			} else {
				_in.reset();
			}
		}
	}

	// -- Serializable --//
	// NOTE: they must be declared as private
	private void writeObject(java.io.ObjectOutputStream s)
			throws java.io.IOException {
		s.defaultWriteObject();
		if (_org != null) {
			// write to buffer
			while(read() != -1);
		}

		close();

		final byte[] data = new byte[_memmaxsz];
		int read;
		while ((read = read(data)) > 0) {
			s.writeInt(read);
			s.write(data, 0, read);
		}
		s.writeInt(0);
	}
	private void readObject(java.io.ObjectInputStream s)
			throws java.io.IOException, ClassNotFoundException {
		s.defaultReadObject();
		int readInt = s.readInt();
		final byte[] data = new byte[_memmaxsz];
		ByteArrayOutputStream out = new ByteArrayOutputStream();
		while (readInt > 0) {
			int read = s.read(data, 0, readInt);
			out.write(data, 0, read);
			readInt -= read;
			if (readInt == 0) {
				readInt = s.readInt();
			}
		}
		_in = new ByteArrayInputStream(out.toByteArray());
	}

	private static class ResetableInputStream extends InputStream
	implements Repeatable, Serializable {
		private final InputStream _org;
		ResetableInputStream(InputStream bais) {
			_org = bais;
		}

		public int read() throws IOException {
			return _org.read();
		}

		@Override
		public int read(byte[] b, int off, int len) throws IOException {
			return _org.read(b, off, len);
		}

		/** Closes the current access, and the next call of {@link #read}
		 * re-opens the buffered input stream.
		 */
		@Override
		public void close() throws IOException {
			_org.reset();
		}
	}
	private static class RepeatableFileInputStream extends InputStream
	implements Repeatable, Serializable {
		private final File _file;
		private InputStream _in;

		RepeatableFileInputStream(File file) {
			_file = file;
		}

		public int read() throws IOException {
			if (_in == null)
				_in = new BufferedInputStream(new FileInputStream(_file));
			return _in.read();
		}

		@Override
		public int read(byte[] b, int off, int len) throws IOException {
			if (_in == null)
				_in = new BufferedInputStream(new FileInputStream(_file));
			return _in.read(b, off, len);
		}

		/** Closes the current access, and the next call of {@link #read}
		 * re-opens the buffered input stream.
		 */
		@Override
		public void close() throws IOException {
			if (_in != null) {
				_in.close();
				_in = null;
			}
		}
	}
	private static class RepeatableURLInputStream extends InputStream
	implements Repeatable, Serializable {
		private final URL _url;
		private InputStream _in;

		RepeatableURLInputStream(URL url) {
			_url = url;
		}

		public int read() throws IOException {
			if (_in == null) {
				_in = _url.openStream();
				_in = new BufferedInputStream(_in);
			}
			return _in.read();
		}

		@Override
		public int read(byte[] b, int off, int len) throws IOException {
			if (_in == null) {
				_in = _url.openStream();
				_in = new BufferedInputStream(_in);
			}
			return _in.read(b, off, len);
		}

		/** Closes the current access, and the next call of {@link #read}
		 * re-opens the buffered input stream.
		 */
		@Override
		public void close() throws IOException {
			if (_in != null) {
				_in.close();
				_in = null;
			}
		}
	}
}
