package ar.com.hjg.pngj.pixels;

import java.io.OutputStream;

import ar.com.hjg.pngj.IDatChunkWriter;

/**
 * This is an OutputStream that compresses (via Deflater or a deflater-like
 * object), and optionally passes the
 * compressed stream to another output stream.
 *
 * It allows to compute in/out/ratio stats.
 *
 * It works as a stream (similar to DeflaterOutputStream), but it's peculiar in
 * that it expects that each writes has a
 * fixed length (other lenghts are accepted, but it's less efficient) and that
 * the total amount of bytes is known (so it
 * can close itself, but it can also be closed on demand) In PNGJ use, the block
 * is typically a row (including filter
 * byte).
 *
 * We use this to do the real compression (with Deflate) but also to compute
 * tentative estimators
 *
 * If not closed, it can be recicled via reset()
 *
 *
 */
public abstract class CompressorStream extends OutputStream {

	protected IDatChunkWriter idatChunkWriter;
	public final int blockLen;
	public final long totalbytes;

	boolean closed = false;
	protected boolean done = false;
	protected long bytesIn = 0;
	protected long bytesOut = 0;
	protected int block = -1;

	/** optionally stores the first byte of each block (row) */
	private byte[] firstBytes;
	protected boolean storeFirstByte = false;

	/**
	 * 
	 * @param idatCw
	 *            Can be null (if we are only interested in compute compression
	 *            ratio)
	 * @param blockLen
	 *            Estimated maximum block length. If unknown, use -1.
	 * @param totalbytes
	 *            Expected total bytes to be fed. If unknown, use -1.
	 */
	public CompressorStream(final IDatChunkWriter idatCw, int blockLen, long totalbytes) {
		idatChunkWriter = idatCw;
		if (blockLen < 0)
			blockLen = 4096;
		if (totalbytes < 0)
			totalbytes = Long.MAX_VALUE;
		if (blockLen < 1 || totalbytes < 1)
			throw new RuntimeException(" maxBlockLen or totalLen invalid");
		this.blockLen = blockLen;
		this.totalbytes = totalbytes;
	}

	/** Releases resources. Idempotent. */
	@Override
	public void close() {
		done();
		if (idatChunkWriter != null)
			idatChunkWriter.close();
		closed = true;
	}

	/**
	 * Will be called automatically when the number of bytes reaches the total
	 * expected Can be also be called from
	 * outside. This should set the flag done=true
	 */
	public abstract void done();

	@Override
	public final void write(final byte[] data) {
		write(data, 0, data.length);
	}

	@Override
	public final void write(final byte[] data, int off, int len) {
		block++;
		if (len <= blockLen) { // normal case
			mywrite(data, off, len);
			if (storeFirstByte && block < firstBytes.length)
				firstBytes[block] = data[off]; // only makes sense in this case
		} else
			while (len > 0) {
				mywrite(data, off, blockLen);
				off += blockLen;
				len -= blockLen;
			}
		if (bytesIn >= totalbytes)
			done();

	}

	/**
	 * same as write, but guarantedd to not exceed blockLen The implementation
	 * should update bytesOut and bytesInt but not
	 * check for totalBytes
	 */
	public abstract void mywrite(byte[] data, int off, int len);

	/**
	 * compressed/raw. This should be called only when done
	 */
	public final double getCompressionRatio() {
		return bytesOut == 0 ? 1.0 : bytesOut / (double) bytesIn;
	}

	/**
	 * raw (input) bytes. This should be called only when done
	 */
	public final long getBytesRaw() {
		return bytesIn;
	}

	/**
	 * compressed (out) bytes. This should be called only when done
	 */
	public final long getBytesCompressed() {
		return bytesOut;
	}

	public boolean isClosed() {
		return closed;
	}

	public boolean isDone() {
		return done;
	}

	public byte[] getFirstBytes() {
		return firstBytes;
	}

	public void setStoreFirstByte(final boolean storeFirstByte, final int nblocks) {
		this.storeFirstByte = storeFirstByte;
		if (this.storeFirstByte) {
			if (firstBytes == null || firstBytes.length < nblocks)
				firstBytes = new byte[nblocks];
		} else
			firstBytes = null;
	}

	public void reset() {
		done();
		bytesIn = 0;
		bytesOut = 0;
		block = -1;
		done = false;
	}

	@Override
	public void write(final int i) { // should not be used
		write(new byte[] { (byte) i });
	}

}
