package com.nifty.cloud.ncs;

import java.io.IOException;
import java.io.InputStream;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * InputStreamをラップするクラスです。
 *
 */
public class NiftyCloudInputStream extends InputStream
{
	private static final Log log = LogFactory.getLog(NiftyCloudInputStream.class);

	private InputStream input = null;
	private int bufferSize = 0;
	private int bufferOffset = 0;
	private long readMark = 0;
	private byte[] buffer = null;

    /**
     * 指定した入力ストリーム、およびバッファサイズを使用してNiftyCloudInputStreamを生成します。
     *
     * @param input
     *  入力ストリーム
     * @param bufferSize
     *  バッファリングするサイズ(byte)
     */
	public NiftyCloudInputStream(InputStream input, int bufferSize)
	{
		if (input == null) {
			throw (new IllegalArgumentException("InputStream can't be null."));
		}

		this.input = input;
		this.bufferSize = bufferSize;
		this.buffer = new byte[bufferSize];

		if (log.isDebugEnabled()) {
			log.debug(String.format("Underlyng input stream will be repeatabme up to %d bytes.", buffer.length));
		}
	}

    /**
     * バッファオフセットによって入力ストリームを開始位置に戻します。
     *
     * @throws IOException
     */
	public void reset() throws IOException
	{
		if (readMark <= bufferSize) {
			if (log.isDebugEnabled()) {
				log.debug(String.format("Reset after reading %d bytes.", readMark));
			}
			readMark = 0;
			bufferOffset = 0;
		} else {
			throw (new IOException(String.format("Input stream can't be reset as %d bytes have been written, exceeding the available buffer size of %d.", readMark, bufferSize)));
		}
	}

	@Override
	public boolean markSupported()
	{
		return (true);
	}

    /**
     * 入力ストリームにマークを設定します。
     * readLimitパラメータは完全に無視されます。
     */
	public synchronized void mark(int readlimit)
	{
		if (log.isDebugEnabled()) {
			log.debug(String.format("Input stream marked at %d bytes.", readMark));
		}

		if (readMark <= bufferSize && buffer != null) {
            /*
             * Clear buffer of already-read data to make more space. It's safe
             * to cast bytesReadPastMark to an int because it is known to be
             * less than bufferSize, which is an int.
             */
			byte[] newBuffer = new byte[this.bufferSize];
			System.arraycopy(buffer, bufferOffset, newBuffer, 0, (int)(readMark - bufferOffset));
			buffer = newBuffer;
			readMark -= bufferOffset;
			bufferOffset = 0;

		} else {
            // If mark is called after the buffer was already exceeded, create a new buffer.
			bufferOffset = 0;
			readMark = 0;
			buffer = new byte[bufferSize];
		}
	}

	@Override
	public int available() throws IOException
	{
		return (input.available());
	}

	@Override
	public void close() throws IOException
	{
		input.close();
	}

	@Override
	public int read(byte[] b, int off, int len) throws IOException
	{
		if (buffer == null) {
			return (input.read(b, off, len));
		}

		if (bufferOffset == readMark) {
			bufferOffset = 0;

			// read from input
			int length = input.read(buffer, bufferOffset, bufferSize);

			if (length <= 0) {
				return (length);
			}

			readMark = length;
		}

        // Check whether we already have buffered data.
		{
            // Data is being repeated, so read from buffer instead of wrapped input stream.
			int readLength = len;
			if (bufferOffset + readLength > readMark) {
				readLength = (int)(readMark - bufferOffset);
			}

			// write to output
			System.arraycopy(buffer, bufferOffset, b, off, readLength);
			bufferOffset += readLength;

			return (readLength);
		}
	}

	@Override
	public int read() throws IOException
	{
		byte[] b = new byte[1];

		int length = read(b);
		if (length != -1) {
			return (b[0]);
		}

		return (length);
	}

	/**
	 * ラップされたInputStreamを取得します。
	 *
	 * @return ラップされたInputStream
	 */
	public InputStream getWrappedInputStream()
	{
		return (input);
	}

}
