package ar.com.hjg.pngj;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.Arrays;

import ar.com.hjg.pngj.ChunkReader.ChunkReaderMode;
import ar.com.hjg.pngj.chunks.ChunkHelper;

/**
 * Consumes a stream of bytes that consist of a series of PNG-like chunks.
 * <p>
 * This has little intelligence, it's quite low-level and general (it could even
 * be used for a MNG stream, for example).
 * It supports signature recognition and idat deflate
 */
public class ChunkSeqReader implements IBytesConsumer {

	protected static final int SIGNATURE_LEN = 8;
	protected final boolean withSignature;

	private final byte[] buf0 = new byte[8]; // for signature or chunk starts
	private int buf0len = 0;

	private boolean signatureDone = false;
	private boolean done = false; // ended, normally or not

	private int chunkCount = 0;

	private long bytesCount = 0;

	private DeflatedChunksSet curReaderDeflatedSet; // one instance is created for each
													// "idat-like set". Normally one.

	private ChunkReader curChunkReader;

	private long idatBytes; // this is only for the IDAT (not mrerely "idat-like")

	/**
	 * Creates a ChunkSeqReader (with signature)
	 */
	public ChunkSeqReader() {
		this(true);
	}

	/**
	 * @param withSignature
	 *            If true, the stream is assumed be prepended by 8 bit signature
	 */
	public ChunkSeqReader(final boolean withSignature) {
		this.withSignature = withSignature;
		signatureDone = !withSignature;
	}

	/**
	 * Consumes (in general, partially) a number of bytes. A single call never
	 * involves more than one chunk.
	 * 
	 * When the signature is read, it calls checkSignature()
	 * 
	 * When the start of a chunk is detected, it calls
	 * {@link #startNewChunk(int, String, long)}
	 * 
	 * When data from a chunk is being read, it delegates to
	 * {@link ChunkReader#feedBytes(byte[], int, int)}
	 * 
	 * The caller might want to call this method more than once in succesion
	 * 
	 * This should rarely be overriden
	 * 
	 * @param buffer
	 * @param offset
	 *            Offset in buffer
	 * @param len
	 *            Valid bytes that can be consumed
	 * @return processed bytes, in the 1-len range. -1 if done. Only returns 0
	 *         if len=0.
	 **/
	@Override
	public int consume(final byte[] buffer, final int offset, final int len) {
		if (done)
			return -1;
		if (len == 0)
			return 0; // nothing to do
		if (len < 0)
			throw new PngjInputException("Bad len: " + len);
		int processed = 0;
		if (signatureDone) {
			if (curChunkReader == null || curChunkReader.isDone()) { // new chunk: read first 8 bytes
				int read0 = 8 - buf0len;
				if (read0 > len)
					read0 = len;
				System.arraycopy(buffer, offset, buf0, buf0len, read0);
				buf0len += read0;
				processed += read0;
				bytesCount += read0;
				// len -= read0;
				// offset += read0;
				if (buf0len == 8) { // end reading chunk length and id
					chunkCount++;
					final int clen = PngHelperInternal.readInt4fromBytes(buf0, 0);
					final String cid = ChunkHelper.toString(buf0, 4, 4);
					startNewChunk(clen, cid, bytesCount - 8);
					buf0len = 0;
				}
			} else { // reading chunk, delegates to curChunkReader
				final int read1 = curChunkReader.feedBytes(buffer, offset, len);
				processed += read1;
				bytesCount += read1;
			}
		} else { // reading signature
			int read = ChunkSeqReader.SIGNATURE_LEN - buf0len;
			if (read > len)
				read = len;
			System.arraycopy(buffer, offset, buf0, buf0len, read);
			buf0len += read;
			if (buf0len == ChunkSeqReader.SIGNATURE_LEN) {
				checkSignature(buf0);
				buf0len = 0;
				signatureDone = true;
			}
			processed += read;
			bytesCount += read;
		}
		return processed;
	}

	/**
	 * Trys to feeds exactly <tt>len</tt> bytes, calling
	 * {@link #consume(byte[], int, int)} retrying if necessary.
	 * 
	 * This should only be used in callback mode
	 * 
	 * @return true if succceded
	 */
	public boolean feedAll(final byte[] buf, int off, int len) {
		while (len > 0) {
			final int n = consume(buf, off, len);
			if (n < 1)
				return false;
			len -= n;
			off += n;
		}
		return true;
	}

	/**
	 * Called for all chunks when a chunk start has been read (id and length),
	 * before the chunk data itself is read. It
	 * creates a new ChunkReader (field accesible via
	 * {@link #getCurChunkReader()}) in the corresponding mode, and
	 * eventually a curReaderDeflatedSet.(field accesible via
	 * {@link #getCurReaderDeflatedSet()})
	 * 
	 * To decide the mode and options, it calls
	 * {@link #shouldCheckCrc(int, String)},
	 * {@link #shouldSkipContent(int, String)}, {@link #isIdatKind(String)}.
	 * Those methods should be overriden in
	 * preference to this; if overriden, this should be called first.
	 * 
	 * The respective {@link ChunkReader#chunkDone()} method is directed to this
	 * {@link #postProcessChunk(ChunkReader)}.
	 * 
	 * Instead of overriding this, see also
	 * {@link #createChunkReaderForNewChunk(String, int, long, boolean)}
	 */
	protected void startNewChunk(final int len, final String id, final long offset) {
		if (id.equals(ChunkHelper.IDAT))
			idatBytes += len;
		final boolean checkCrc = shouldCheckCrc(len, id);
		final boolean skip = shouldSkipContent(len, id);
		final boolean isIdatType = isIdatKind(id);
		// PngHelperInternal.debug("start new chunk  id=" + id + " off=" + offset + " skip=" + skip + " idat=" +
		// isIdatType);
		// first see if we should terminate an active curReaderDeflatedSet
		boolean forCurrentIdatSet = false;
		if (curReaderDeflatedSet != null)
			forCurrentIdatSet = curReaderDeflatedSet.ackNextChunkId(id);
		if (isIdatType && !skip) { // IDAT non skipped: create a DeflatedChunkReader owned by a idatSet
			if (!forCurrentIdatSet) {
				if (curReaderDeflatedSet != null && !curReaderDeflatedSet.isDone())
					throw new PngjInputException("new IDAT-like chunk when previous was not done");
				curReaderDeflatedSet = createIdatSet(id);
			}
			curChunkReader = new DeflatedChunkReader(len, id, checkCrc, offset, curReaderDeflatedSet) {
				@Override
				protected void chunkDone() {
					super.chunkDone();
					postProcessChunk(this);
				}
			};

		} else { // for non-idat chunks (or skipped idat like)
			curChunkReader = createChunkReaderForNewChunk(id, len, offset, skip);
			if (!checkCrc)
				curChunkReader.setCrcCheck(false);
		}
	}

	/**
	 * This will be called for all chunks (even skipped), except for IDAT-like
	 * non-skiped chunks
	 * 
	 * The default behaviour is to create a ChunkReader in BUFFER mode (or SKIP
	 * if skip==true) that calls
	 * {@link #postProcessChunk(ChunkReader)} (always) when done.
	 * 
	 * @param id
	 *            Chunk id
	 * @param len
	 *            Chunk length
	 * @param offset
	 *            offset inside PNG stream , merely informative
	 * @param skip
	 *            flag: is true, the content will not be buffered (nor
	 *            processed)
	 * @return a newly created ChunkReader that will create the ChunkRaw and
	 *         then discarded
	 */
	protected ChunkReader createChunkReaderForNewChunk(final String id, final int len, final long offset,
			final boolean skip) {
		return new ChunkReader(len, id, offset, skip ? ChunkReaderMode.SKIP : ChunkReaderMode.BUFFER) {
			@Override
			protected void chunkDone() {
				postProcessChunk(this);
			}

			@Override
			protected void processData(final int offsetinChhunk, final byte[] buf, final int off, final int len) {
				throw new PngjExceptionInternal("should never happen");
			}
		};
	}

	/**
	 * This is called after a chunk is read, in all modes
	 * 
	 * This implementation only chenks the id of the first chunk, and process
	 * the IEND chunk (sets done=true)
	 ** 
	 * Further processing should be overriden (call this first!)
	 **/
	protected void postProcessChunk(final ChunkReader chunkR) { // called after chunk is read
		if (chunkCount == 1) {
			final String cid = firstChunkId();
			if (cid != null && !cid.equals(chunkR.getChunkRaw().id))
				throw new PngjInputException("Bad first chunk: " + chunkR.getChunkRaw().id + " expected: " + firstChunkId());
		}
		if (chunkR.getChunkRaw().id.equals(endChunkId()))
			done = true;
	}

	/**
	 * DeflatedChunksSet factory. This implementation is quite dummy, it usually
	 * should be overriden.
	 */
	protected DeflatedChunksSet createIdatSet(final String id) {
		return new DeflatedChunksSet(id, 1024, 1024); // sizes: arbitrary This should normally be
														// overriden
	}

	/**
	 * Decides if this Chunk is of "IDAT" kind (in concrete: if it is, and if
	 * it's not to be skiped, a DeflatedChunksSet
	 * will be created to deflate it and process+ the deflated data)
	 * 
	 * This implementation always returns always false
	 * 
	 * @param id
	 */
	protected boolean isIdatKind(final String id) {
		return false;
	}

	/**
	 * Chunks can be skipped depending on id and/or length. Skipped chunks are
	 * still processed, but their data will be
	 * null, and CRC will never checked
	 * 
	 * @param len
	 * @param id
	 */
	protected boolean shouldSkipContent(final int len, final String id) {
		return false;
	}

	protected boolean shouldCheckCrc(final int len, final String id) {
		return true;
	}

	/**
	 * Throws PngjInputException if bad signature
	 * 
	 * @param buf
	 *            Signature. Should be of length 8
	 */
	protected void checkSignature(final byte[] buf) {
		if (!Arrays.equals(buf, PngHelperInternal.getPngIdSignature()))
			throw new PngjInputException("Bad PNG signature");
	}

	/**
	 * If false, we are still reading the signature
	 * 
	 * @return true if signature has been read (or if we don't have signature)
	 */
	public boolean isSignatureDone() {
		return signatureDone;
	}

	/**
	 * If true, we either have processe the IEND chunk, or close() has been
	 * called, or a fatal error has happened
	 */
	public boolean isDone() {
		return done;
	}

	/**
	 * total of bytes read (buffered or not)
	 */
	public long getBytesCount() {
		return bytesCount;
	}

	/**
	 * @return Chunks already read, including partial reading (currently
	 *         reading)
	 */
	public int getChunkCount() {
		return chunkCount;
	}

	/**
	 * Currently reading chunk, or just ended reading
	 * 
	 * @return null only if still reading signature
	 */
	public ChunkReader getCurChunkReader() {
		return curChunkReader;
	}

	/**
	 * The latest deflated set (typically IDAT chunks) reader. Notice that there
	 * could be several idat sets (eg for APNG)
	 */
	public DeflatedChunksSet getCurReaderDeflatedSet() {
		return curReaderDeflatedSet;
	}

	/**
	 * Closes this object and release resources. For normal termination or
	 * abort. Secure and idempotent.
	 */
	public void close() { // forced closing
		if (curReaderDeflatedSet != null)
			curReaderDeflatedSet.close();
		done = true;
	}

	/**
	 * Returns true if we are not in middle of a chunk: we have just ended
	 * reading past chunk , or we are at the start, or
	 * end of signature, or we are done
	 */
	public boolean isAtChunkBoundary() {
		return bytesCount == 0 || bytesCount == 8 || done || curChunkReader == null || curChunkReader.isDone();
	}

	/**
	 * Which should be the id of the first chunk
	 * 
	 * @return null if you don't want to check it
	 */
	protected String firstChunkId() {
		return "IHDR";
	}

	/**
	 * Helper method, reports amount of bytes inside IDAT chunks.
	 * 
	 * @return Bytes in IDAT chunks
	 */
	public long getIdatBytes() {
		return idatBytes;
	}

	/**
	 * Which should be the id of the last chunk
	 * 
	 * @return "IEND"
	 */
	protected String endChunkId() {
		return "IEND";
	}

	/**
	 * Reads all content from a file. Helper method, only for callback mode
	 */
	public void feedFromFile(final File f) {
		try {
			feedFromInputStream(new FileInputStream(f), true);
		} catch (final FileNotFoundException e) {
			throw new PngjInputException(e.getMessage());
		}
	}

	/**
	 * Reads all content from an input stream. Helper method, only for callback
	 * mode
	 * 
	 * @param is
	 * @param closeStream
	 *            Closes the input stream when done (or if error)
	 */
	public void feedFromInputStream(final InputStream is, final boolean closeStream) {
		final BufferedStreamFeeder sf = new BufferedStreamFeeder(is);
		sf.setCloseStream(closeStream);
		try {
			sf.feedAll(this);
		} finally {
			close();
			sf.close();
		}
	}

	public void feedFromInputStream(final InputStream is) {
		feedFromInputStream(is, true);
	}
}
