package ar.com.hjg.pngj;

import ar.com.hjg.pngj.chunks.ChunkRaw;

/**
 * Parses a PNG chunk, consuming bytes in one mode:
 * {@link ChunkReaderMode#BUFFER}, {@link ChunkReaderMode#PROCESS},
 * {@link ChunkReaderMode#SKIP}.
 * <p>
 * It calls {@link #chunkDone()} when done. Also calls
 * {@link #processData(byte[], int, int)} if <code>PROCESS</code>
 * mode. Apart from thas, it's totally agnostic (it doesn't know about IDAT
 * chunks, or PNG general structure)
 * <p>
 * The object wraps a ChunkRaw instance (content filled only if BUFFER mode); it
 * should be short lived (one instance
 * created for each chunk, and discarded after reading), but the wrapped
 * chunkRaw can be (usually is) long lived.
 */
public abstract class ChunkReader {

	/**
	 * see {@link ChunkReaderMode}
	 */
	public final ChunkReaderMode mode;
	private final ChunkRaw chunkRaw;

	private boolean crcCheck; // by default, this is false for SKIP, true elsewhere

	/**
	 * How many bytes have been read for this chunk, data only
	 */
	protected int read = 0;
	private int crcn = 0; // how many bytes have been read from crc

	/**
	 * Modes of ChunkReader chunk processing.
	 */
	public enum ChunkReaderMode {
		/**
		 * Stores full chunk data in buffer
		 */
		BUFFER,
		/**
		 * Does not store content, processes on the fly, calling processData()
		 * for each partial read
		 */
		PROCESS,
		/**
		 * Does not store nor process - implies crcCheck=false (by default).
		 */
		SKIP;
	}

	/**
	 * The constructor creates also a chunkRaw, preallocated if mode =
	 * ChunkReaderMode.BUFFER
	 * 
	 * @param clen
	 * @param id
	 * @param offsetInPng
	 *            Informational, is stored in chunkRaw
	 * @param mode
	 */
	public ChunkReader(final int clen, final String id, final long offsetInPng, final ChunkReaderMode mode) {
		if (mode == null || id.length() != 4 || clen < 0)
			throw new PngjExceptionInternal("Bad chunk paramenters: " + mode);
		this.mode = mode;
		chunkRaw = new ChunkRaw(clen, id, mode == ChunkReaderMode.BUFFER);
		chunkRaw.setOffset(offsetInPng);
		crcCheck = mode == ChunkReaderMode.SKIP ? false : true; // can be changed with setter
	}

	/**
	 * Returns raw chunk (data can be empty or not, depending on
	 * ChunkReaderMode)
	 * 
	 * @return Raw chunk - never null
	 */
	public ChunkRaw getChunkRaw() {
		return chunkRaw;
	}

	/**
	 * Consumes data for the chunk (data and CRC). This never consumes more
	 * bytes than for this chunk.
	 * 
	 * In HOT_PROCESS can call processData() (not more than once)
	 * 
	 * If this ends the chunk (included CRC) it checks CRC (if checking) and
	 * calls chunkDone()
	 * 
	 * @param buf
	 * @param off
	 * @param len
	 * @return How many bytes have been consumed
	 */
	public final int feedBytes(final byte[] buf, int off, int len) {
		if (len == 0)
			return 0;
		if (len < 0)
			throw new PngjException("negative length??");
		if (read == 0 && crcn == 0 && crcCheck)
			chunkRaw.updateCrc(chunkRaw.idbytes, 0, 4); // initializes crc calculation with the Chunk ID
		int bytesForData = chunkRaw.len - read; // bytesForData : bytes to be actually read from chunk data
		if (bytesForData > len)
			bytesForData = len;
		// we want to call processData even for empty chunks (IEND:bytesForData=0) at least once
		if (bytesForData > 0 || crcn == 0) {
			// in buffer mode we compute the CRC at the end
			if (crcCheck && mode != ChunkReaderMode.BUFFER && bytesForData > 0)
				chunkRaw.updateCrc(buf, off, bytesForData);

			if (mode == ChunkReaderMode.BUFFER) {
				// just copy the contents to the internal buffer
				if (chunkRaw.data != buf && bytesForData > 0)
					// if the buffer passed if the same as this one, we don't copy the caller should know what he's doing
					System.arraycopy(buf, off, chunkRaw.data, read, bytesForData);
			} else if (mode == ChunkReaderMode.PROCESS)
				processData(read, buf, off, bytesForData);
			else {
				// mode == ChunkReaderMode.SKIP; nothing to do
			}
			read += bytesForData;
			off += bytesForData;
			len -= bytesForData;
		}
		int crcRead = 0;
		if (read == chunkRaw.len) { // data done - read crc?
			crcRead = 4 - crcn;
			if (crcRead > len)
				crcRead = len;
			if (crcRead > 0) {
				if (buf != chunkRaw.crcval)
					System.arraycopy(buf, off, chunkRaw.crcval, crcn, crcRead);
				crcn += crcRead;
				if (crcn == 4) {
					if (crcCheck) {
						if (mode == ChunkReaderMode.BUFFER)
							chunkRaw.updateCrc(chunkRaw.data, 0, chunkRaw.len);
						chunkRaw.checkCrc();
					}
					chunkDone();
				}
			}
		}
		return bytesForData + crcRead;
	}

	/**
	 * Chunks has been read
	 * 
	 * @return true if we have read all chunk, including trailing CRC
	 */
	public final boolean isDone() {
		return crcn == 4; // has read all 4 bytes from the crc
	}

	/**
	 * Determines if CRC should be checked. This should be called before
	 * starting reading.
	 * 
	 * @param crcCheck
	 */
	public void setCrcCheck(final boolean crcCheck) {
		if (read != 0 && crcCheck && !this.crcCheck)
			throw new PngjException("too late!");
		this.crcCheck = crcCheck;
	}

	/**
	 * This method will only be called in PROCESS mode, probably several times,
	 * each time with a new fragment of data
	 * inside the chunk. For chunks with zero-length data, this will still be
	 * called once.
	 * 
	 * It's guaranteed that the data corresponds exclusively to this chunk data
	 * (no crc, no data from no other chunks, )
	 * 
	 * @param offsetInchunk
	 *            data bytes that had already been read/processed for this chunk
	 * @param buf
	 * @param off
	 * @param len
	 */
	protected abstract void processData(int offsetInchunk, byte[] buf, int off, int len);

	/**
	 * This method will be called (in all modes) when the full chunk -including
	 * crc- has been read
	 */
	protected abstract void chunkDone();

	public boolean isFromDeflatedSet() {
		return false;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + (chunkRaw == null ? 0 : chunkRaw.hashCode());
		return result;
	}

	/**
	 * Equality (and hash) is basically delegated to the ChunkRaw
	 */
	@Override
	public boolean equals(final Object obj) { // delegates to chunkraw
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		final ChunkReader other = (ChunkReader) obj;
		if (chunkRaw == null) {
			if (other.chunkRaw != null)
				return false;
		} else if (!chunkRaw.equals(other.chunkRaw))
			return false;
		return true;
	}

	@Override
	public String toString() {
		return chunkRaw.toString();
	}

}
