/* RepeatableReader.java

	Purpose:
		
	Description:
		
	History:
		Fri Mar 14 11:47:38     2008, Created by tomyeh

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.CharArrayReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Reader;
import java.io.Serializable;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URL;
import java.nio.file.Files;

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

/**
 * {@link RepeatableReader} adds functionality to another reader,
 * the ability to read repeatedly.
 * By repeatable-read we mean, after {@link #close}, the next invocation of
 * {@link #read} will re-open the reader.
 *
 * <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 reader)
 * is never closed until garbage-collected.
 *
 * <p>If the content size of the given reader 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 reader 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 tomyeh
 * @since 3.0.4
 */
public class RepeatableReader extends Reader implements Repeatable, Serializable {
	private static final Logger log = LoggerFactory.getLogger(RepeatableReader.class);

	private Reader _org;
	private Writer _out;
	private Reader _in;
	private 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 RepeatableReader(Reader is) {
		_org = is;
		_bufmaxsz = Library.getIntProperty(
			RepeatableInputStream.BUFFER_LIMIT_SIZE, 20 * 1024 * 1024);
		_memmaxsz = Library.getIntProperty(
			RepeatableInputStream.MEMORY_LIMIT_SIZE, 512 * 1024);
	}

	/**
	 * Returns a reader that can be read repeatedly, or null if the given
	 * reader is null.
	 * Note: the returned reader encapsulates the given reader, rd
	 * (a.k.a., the buffered reader) to adds the functionality to
	 * re-opens the reader once {@link #close} is called.
	 *
	 * <p>By repeatable-read we mean, after {@link #close}, the next
	 * invocation of {@link #read} will re-open the reader.
	 *
	 * <p>Use this method instead of instantiating {@link RepeatableReader}
	 * with the constructor.
	 *
	 * @see #getInstance(File)
	 */
	public static Reader getInstance(Reader rd) {
		if ((rd instanceof CharArrayReader) || (rd instanceof StringReader))
			return new ResetableReader(rd);
		else if (rd != null && !(rd instanceof Repeatable))
			return new RepeatableReader(rd);
		return rd;
	}
	/**
	 * Returns a reader to read a file that can be read repeatedly.
	 * Note: it assumes the file is text (rather than binary).
	 *
	 * <p>By repeatable-read we mean, after {@link #close}, the next
	 * invocation of {@link #read} will re-open the reader.
	 *
	 * <p>Note: it is efficient since we don't have to buffer the
	 * content of the file to make it repeatable-read.
	 *
	 * @param charset the charset. If null, "UTF-8" is assumed.
	 * @exception IllegalArgumentException if file is null.
	 * @see #getInstance(Reader)
	 * @see #getInstance(String, String)
	 * @since 3.0.8
	 */
	public static Reader getInstance(File file, String charset)
	throws FileNotFoundException {
		if (file == null)
			throw new IllegalArgumentException("null");
		if (!file.exists())
			throw new FileNotFoundException(file.toString());
		return new RepeatableFileReader(file, charset);
	}
	/**
	 * Returns a reader to read a file, encoded in UTF-8,
	 * that can be read repeatedly.
	 * Note: it assumes the file is text (rather than binary).
	 *
	 * <p>By repeatable-read we mean, after {@link #close}, the next
	 * invocation of {@link #read} will re-open the reader.
	 *
	 * <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(Reader)
	 * @see #getInstance(String)
	 */
	public static Reader getInstance(File file)
	throws FileNotFoundException {
		return getInstance(file, "UTF-8");
	}
	/**
	 * Returns a reader to read a file that can be read repeatedly.
	 * Note: it assumes the file is text (rather than binary).
	 *
	 * <p>By repeatable-read we mean, after {@link #close}, the next
	 * invocation of {@link #read} will re-open the reader.
	 *
	 * <p>Note: it is efficient since we don't have to buffer the
	 * content of the file to make it repeatable-read.
	 *
	 * @param filename the file name
	 * @param charset the charset. If null, "UTF-8" is assumed.
	 * @exception IllegalArgumentException if file is null.
	 * @exception FileNotFoundException if file is not found.
	 * @see #getInstance(Reader)
	 * @see #getInstance(File, String)
	 * @since 3.0.8
	 */
	public static Reader getInstance(String filename, String charset)
	throws FileNotFoundException {
		return getInstance(new File(filename));
	}
	/**
	 * Returns a reader to read a file, encoded in UTF-8,
	 * that can be read repeatedly.
	 * Note: it assumes the file is text (rather than binary).
	 *
	 * <p>By repeatable-read we mean, after {@link #close}, the next
	 * invocation of {@link #read} will re-open the reader.
	 *
	 * <p>Note: it is efficient since we don't have to buffer the
	 * content of the file to make it repeatable-read.
	 *
	 * @param filename the file name
	 * @exception IllegalArgumentException if file is null.
	 * @exception FileNotFoundException if file is not found.
	 * @see #getInstance(Reader)
	 * @see #getInstance(File)
	 */
	public static Reader getInstance(String filename)
	throws FileNotFoundException {
		return getInstance(new File(filename), "UTF-8");
	}
	/**
	 * Returns a reader to read the resource of the specified URL.
	 * The reader can be read repeatedly.
	 * Note: it assumes the resource is text (rather than binary).
	 *
	 * <p>By repeatable-read we mean, after {@link #close}, the next
	 * invocation of {@link #read} will re-open the reader.
	 *
	 * <p>Note: it is efficient since we don't have to buffer the
	 * content of the file to make it repeatable-read.
	 *
	 * @param charset the charset. If null, "UTF-8" is assumed.
	 * @exception IllegalArgumentException if file is null.
	 * @see #getInstance(Reader)
	 * @see #getInstance(String, String)
	 * @since 3.0.8
	 */
	public static Reader getInstance(URL url, String charset) {
		if (url == null)
			throw new IllegalArgumentException("null");
		return new RepeatableURLReader(url, charset);
	}
	/**
	 * Returns a reader to read the resource of the specified URL,
	 * encoded in UTF-8.
	 * The reader can be read repeatedly.
	 * Note: it assumes the resource is text (rather than binary).
	 *
	 * <p>By repeatable-read we mean, after {@link #close}, the next
	 * invocation of {@link #read} will re-open the reader.
	 *
	 * <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(Reader)
	 * @see #getInstance(String)
	 */
	public static Reader getInstance(URL url) {
		return getInstance(url, "UTF-8");
	}

	private Writer getWriter() throws IOException {
		if (_out == null)
			return _nobuf ? null: (_out = new StringWriter());
				//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 String cnt = ((StringWriter)_out).toString();
				_out = new FileWriter(_f, "UTF-8");
				_out.write(cnt);
			} catch (Throwable ex) {
				log.warn("Ignored: failed to buffer to a file, "+_f+"\nCause: "+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(char cbuf[], int off, int len) throws IOException {
		if (_org != null) {
			final int cnt = _org.read(cbuf, off, len);
			if (!_nobuf)
				if (cnt >= 0) {
					final Writer out = getWriter();
					if (out != null) out.write(cbuf, off, cnt);
					_cntsz += cnt;
				}
			return cnt;
		} else {
			if (_in == null)
				_in = new FileReader(_f, "UTF-8"); //_f must be non-null

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

	/** Closes the current access, and the next call of {@link #read}
	 * re-opens the buffered reader.
	 */
	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 StringReader(
						((StringWriter)_out).toString());
					//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();
			}
		}
	}

	//Object//
	protected void finalize() throws Throwable {
		disableBuffering();
		if (_org != null)
			_org.close();
		if (_in != null) {
			_in.close();
			_in = null;
		}
		super.finalize();
	}

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

		public int read(char cbuf[], int off, int len) throws IOException {
			return _org.read(cbuf, off, len);
		}
		/** Closes the current access and the next call of {@link #read}
		 * re-opens the buffered reader.
		 */
		public void close() throws IOException {
			_org.reset();
		}

		//Object//
		protected void finalize() throws Throwable {
			_org.close();
			super.finalize();
		}
	}
	private static class RepeatableFileReader extends Reader implements Repeatable,
			Serializable {
		private final File _file;
		private Reader _in;
		private final String _charset;

		RepeatableFileReader(File file, String charset) {
			_file = file;
			_charset = charset != null ? charset: "UTF-8";
		}

		public int read(char cbuf[], int off, int len) throws IOException {
			if (_in == null)
				_in = new FileReader(_file, _charset);
			return _in.read(cbuf, off, len);
		}
		/** Closes the current access and the next call of {@link #read}
		 * re-opens the buffered reader.
		 */
		public void close() throws IOException {
			if (_in != null) {
				_in.close();
				_in = null;
			}
		}

		//Object//
		protected void finalize() throws Throwable {
			close();
			super.finalize();
		}
	}
	private static class RepeatableURLReader extends Reader implements Repeatable,
			Serializable {
		private final URL _url;
		private Reader _in;
		private final String _charset;

		RepeatableURLReader(URL url, String charset) {
			_url = url;
			_charset = charset != null ? charset: "UTF-8";
		}

		public int read(char cbuf[], int off, int len) throws IOException {
			if (_in == null)
				_in = new URLReader(_url, _charset);
			return _in.read(cbuf, off, len);
		}
		/** Closes the current access and the next call of {@link #read}
		 * re-opens the buffered reader.
		 */
		public void close() throws IOException {
			if (_in != null) {
				_in.close();
				_in = null;
			}
		}

		//Object//
		protected void finalize() throws Throwable {
			close();
			super.finalize();
		}
	}
}
