package ar.com.hjg.pngj.chunks;

import java.io.ByteArrayInputStream;
import java.io.OutputStream;
import java.util.zip.CRC32;

import ar.com.hjg.pngj.PngHelperInternal;
import ar.com.hjg.pngj.PngjBadCrcException;
import ar.com.hjg.pngj.PngjException;
import ar.com.hjg.pngj.PngjOutputException;

/**
 * Raw (physical) chunk.
 * <p>
 * Short lived object, to be created while serialing/deserializing Do not reuse
 * it for different chunks. <br>
 * See http://www.libpng.org/pub/png/spec/1.2/PNG-Structure.html
 */
public class ChunkRaw {
	/**
	 * The length counts only the data field, not itself, the chunk type code,
	 * or the CRC. Zero is a valid length.
	 * Although encoders and decoders should treat the length as unsigned, its
	 * value must not exceed 231-1 bytes.
	 */
	public final int len;

	/**
	 * A 4-byte chunk type code. uppercase and lowercase ASCII letters
	 */
	public final byte[] idbytes;
	public final String id;

	/**
	 * The data bytes appropriate to the chunk type, if any. This field can be
	 * of zero length. Does not include crc. If
	 * it's null, it means that the data is ot available
	 */
	public byte[] data = null;
	/**
	 * @see ChunkRaw#getOffset()
	 */
	private long offset = 0;

	/**
	 * A 4-byte CRC (Cyclic Redundancy Check) calculated on the preceding bytes
	 * in the chunk, including the chunk type
	 * code and chunk data fields, but not including the length field.
	 */
	public byte[] crcval = new byte[4];

	private CRC32 crcengine; // lazily instantiated

	public ChunkRaw(final int len, final String id, final boolean alloc) {
		this.len = len;
		this.id = id;
		idbytes = ChunkHelper.toBytes(id);
		for (int i = 0; i < 4; i++)
			if (idbytes[i] < 65 || idbytes[i] > 122 || idbytes[i] > 90 && idbytes[i] < 97)
				throw new PngjException("Bad id chunk: must be ascii letters " + id);
		if (alloc)
			allocData();
	}

	public ChunkRaw(final int len, final byte[] idbytes, final boolean alloc) {
		this(len, ChunkHelper.toString(idbytes), alloc);
	}

	public void allocData() { // TODO: not public
		if (data == null || data.length < len)
			data = new byte[len];
	}

	/**
	 * this is called after setting data, before writing to os
	 */
	private void computeCrcForWriting() {
		crcengine = new CRC32();
		crcengine.update(idbytes, 0, 4);
		if (len > 0)
			crcengine.update(data, 0, len); //
		PngHelperInternal.writeInt4tobytes((int) crcengine.getValue(), crcval, 0);
	}

	/**
	 * Computes the CRC and writes to the stream. If error, a
	 * PngjOutputException is thrown
	 * 
	 * Note that this is only used for non idat chunks
	 */
	public void writeChunk(final OutputStream os) {
		writeChunkHeader(os);
		if (len > 0) {
			if (data == null)
				throw new PngjOutputException("cannot write chunk, raw chunk data is null [" + id + "]");
			PngHelperInternal.writeBytes(os, data, 0, len);
		}
		computeCrcForWriting();
		writeChunkCrc(os);
	}

	public void writeChunkHeader(final OutputStream os) {
		if (idbytes.length != 4)
			throw new PngjOutputException("bad chunkid [" + id + "]");
		PngHelperInternal.writeInt4(os, len);
		PngHelperInternal.writeBytes(os, idbytes);
	}

	public void writeChunkCrc(final OutputStream os) {
		PngHelperInternal.writeBytes(os, crcval, 0, 4);
	}

	public void checkCrc() {
		final int crcComputed = (int) crcengine.getValue();
		final int crcExpected = PngHelperInternal.readInt4fromBytes(crcval, 0);
		if (crcComputed != crcExpected)
			throw new PngjBadCrcException("chunk: " + toString() + " expected=" + crcExpected + " read=" + crcComputed);
	}

	public void updateCrc(final byte[] buf, final int off, final int len) {
		if (crcengine == null)
			crcengine = new CRC32();
		crcengine.update(buf, off, len);
	}

	ByteArrayInputStream getAsByteStream() { // only the data
		return new ByteArrayInputStream(data);
	}

	/**
	 * offset in the full PNG stream, in bytes. only informational, for read
	 * chunks (0=NA)
	 */
	public long getOffset() {
		return offset;
	}

	public void setOffset(final long offset) {
		this.offset = offset;
	}

	@Override
	public String toString() {
		return "chunkid=" + ChunkHelper.toString(idbytes) + " len=" + len;
	}

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

	@Override
	public boolean equals(final Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		final ChunkRaw other = (ChunkRaw) obj;
		if (id == null) {
			if (other.id != null)
				return false;
		} else if (!id.equals(other.id))
			return false;
		if (offset != other.offset)
			return false;
		return true;
	}

}
