package ar.com.hjg.pngj;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

import ar.com.hjg.pngj.ChunkReader.ChunkReaderMode;
import ar.com.hjg.pngj.chunks.ChunkFactory;
import ar.com.hjg.pngj.chunks.ChunkHelper;
import ar.com.hjg.pngj.chunks.ChunkLoadBehaviour;
import ar.com.hjg.pngj.chunks.ChunksList;
import ar.com.hjg.pngj.chunks.PngChunk;
import ar.com.hjg.pngj.chunks.PngChunkIDAT;
import ar.com.hjg.pngj.chunks.PngChunkIEND;
import ar.com.hjg.pngj.chunks.PngChunkIHDR;
import ar.com.hjg.pngj.chunks.PngChunkPLTE;

/**
 * Adds to ChunkSeqReader the storing of PngChunk, with a PngFactory, and
 * imageInfo + deinterlacer.
 * <p>
 * Most usual PNG reading should use this class, or a {@link PngReader}, which
 * is a thin wrapper over this.
 */
public class ChunkSeqReaderPng extends ChunkSeqReader {

	protected ImageInfo imageInfo; // initialized at parsing the IHDR
	protected ImageInfo curImageInfo; // can vary, for apng
	protected Deinterlacer deinterlacer;
	protected int currentChunkGroup = -1;

	/**
	 * All chunks, but some of them can have the buffer empty (IDAT and skipped)
	 */
	protected ChunksList chunksList = null;
	protected final boolean callbackMode;
	private long bytesAncChunksLoaded = 0; // bytes loaded from buffered chunks non-critical chunks (data only)

	private boolean checkCrc = true;

	// --- parameters to be set prior to reading ---
	private boolean includeNonBufferedChunks = false;

	private final Set<String> chunksToSkip = new HashSet<>();
	private long maxTotalBytesRead = 0;
	private long skipChunkMaxSize = 0;
	private long maxBytesMetadata = 0;
	private IChunkFactory chunkFactory;
	private ChunkLoadBehaviour chunkLoadBehaviour = ChunkLoadBehaviour.LOAD_CHUNK_ALWAYS;

	public ChunkSeqReaderPng(final boolean callbackMode) {
		super();
		this.callbackMode = callbackMode;
		chunkFactory = new ChunkFactory(); // default factory
	}

	private void updateAndCheckChunkGroup(final String id) {
		if (id.equals(PngChunkIHDR.ID)) { // IDHR
			if (currentChunkGroup < 0)
				currentChunkGroup = ChunksList.CHUNK_GROUP_0_IDHR;
			else
				throw new PngjInputException("unexpected chunk " + id);
		} else if (id.equals(PngChunkPLTE.ID)) { // PLTE
			if (currentChunkGroup == ChunksList.CHUNK_GROUP_0_IDHR || currentChunkGroup == ChunksList.CHUNK_GROUP_1_AFTERIDHR)
				currentChunkGroup = ChunksList.CHUNK_GROUP_2_PLTE;
			else
				throw new PngjInputException("unexpected chunk " + id);
		} else if (id.equals(PngChunkIDAT.ID)) { // IDAT (no necessarily the first)
			if (currentChunkGroup >= ChunksList.CHUNK_GROUP_0_IDHR && currentChunkGroup <= ChunksList.CHUNK_GROUP_4_IDAT)
				currentChunkGroup = ChunksList.CHUNK_GROUP_4_IDAT;
			else
				throw new PngjInputException("unexpected chunk " + id);
		} else if (id.equals(PngChunkIEND.ID)) { // END
			if (currentChunkGroup >= ChunksList.CHUNK_GROUP_4_IDAT)
				currentChunkGroup = ChunksList.CHUNK_GROUP_6_END;
			else
				throw new PngjInputException("unexpected chunk " + id);
		} else if (currentChunkGroup <= ChunksList.CHUNK_GROUP_1_AFTERIDHR)
			currentChunkGroup = ChunksList.CHUNK_GROUP_1_AFTERIDHR;
		else if (currentChunkGroup <= ChunksList.CHUNK_GROUP_3_AFTERPLTE)
			currentChunkGroup = ChunksList.CHUNK_GROUP_3_AFTERPLTE;
		else
			currentChunkGroup = ChunksList.CHUNK_GROUP_5_AFTERIDAT;
	}

	@Override
	public boolean shouldSkipContent(final int len, final String id) {
		if (super.shouldSkipContent(len, id))
			return true;
		if (ChunkHelper.isCritical(id))
			return false;// critical chunks are never skipped
		if (maxTotalBytesRead > 0 && len + getBytesCount() > maxTotalBytesRead)
			throw new PngjInputException("Maximum total bytes to read exceeeded: " + maxTotalBytesRead + " offset:" + getBytesCount() + " len=" + len);
		if (chunksToSkip.contains(id))
			return true; // specific skip
		if (skipChunkMaxSize > 0 && len > skipChunkMaxSize)
			return true; // too big chunk
		if (maxBytesMetadata > 0 && len > maxBytesMetadata - bytesAncChunksLoaded)
			return true; // too much ancillary chunks loaded
		switch (chunkLoadBehaviour) {
			case LOAD_CHUNK_IF_SAFE:
				if (!ChunkHelper.isSafeToCopy(id))
					return true;
				break;
			case LOAD_CHUNK_NEVER:
				return true;
			default:
				break;
		}
		return false;
	}

	public long getBytesChunksLoaded() {
		return bytesAncChunksLoaded;
	}

	public int getCurrentChunkGroup() {
		return currentChunkGroup;
	}

	public void setChunksToSkip(final String... chunksToSkip) {
		this.chunksToSkip.clear();
		for (final String c : chunksToSkip)
			this.chunksToSkip.add(c);
	}

	public void addChunkToSkip(final String chunkToSkip) {
		chunksToSkip.add(chunkToSkip);
	}

	public void dontSkipChunk(final String chunkToSkip) {
		chunksToSkip.remove(chunkToSkip);
	}

	public boolean firstChunksNotYetRead() {
		return getCurrentChunkGroup() < ChunksList.CHUNK_GROUP_4_IDAT;
	}

	@Override
	protected void postProcessChunk(final ChunkReader chunkR) {
		super.postProcessChunk(chunkR);
		if (chunkR.getChunkRaw().id.equals(PngChunkIHDR.ID)) {
			final PngChunkIHDR ch = new PngChunkIHDR(null);
			ch.parseFromRaw(chunkR.getChunkRaw());
			imageInfo = ch.createImageInfo();
			curImageInfo = imageInfo;
			if (ch.isInterlaced())
				deinterlacer = new Deinterlacer(curImageInfo);
			chunksList = new ChunksList(imageInfo);
		}
		if (chunkR.mode == ChunkReaderMode.BUFFER && countChunkTypeAsAncillary(chunkR.getChunkRaw().id))
			bytesAncChunksLoaded += chunkR.getChunkRaw().len;
		if (chunkR.mode == ChunkReaderMode.BUFFER || includeNonBufferedChunks) {
			final PngChunk chunk = chunkFactory.createChunk(chunkR.getChunkRaw(), getImageInfo());
			chunksList.appendReadChunk(chunk, currentChunkGroup);
		}
		if (isDone())
			processEndPng();
	}

	protected boolean countChunkTypeAsAncillary(final String id) {
		return !ChunkHelper.isCritical(id);
	}

	@Override
	protected DeflatedChunksSet createIdatSet(final String id) {
		final IdatSet ids = new IdatSet(id, getCurImgInfo(), deinterlacer);
		ids.setCallbackMode(callbackMode);
		return ids;
	}

	public IdatSet getIdatSet() {
		final DeflatedChunksSet c = getCurReaderDeflatedSet();
		return c instanceof IdatSet ? (IdatSet) c : null;
	}

	@Override
	protected boolean isIdatKind(final String id) {
		return id.equals(PngChunkIDAT.ID);
	}

	@Override
	public int consume(final byte[] buf, final int off, final int len) {
		return super.consume(buf, off, len);
	}

	/**
	 * sets a custom chunk factory. This is typically called with a custom class
	 * extends ChunkFactory, to adds custom
	 * chunks to the default well-know ones
	 * 
	 * @param chunkFactory
	 */
	public void setChunkFactory(final IChunkFactory chunkFactory) {
		this.chunkFactory = chunkFactory;
	}

	/**
	 * Things to be done after IEND processing. This is not called if
	 * prematurely closed.
	 */
	protected void processEndPng() {
		// nothing to do
	}

	public ImageInfo getImageInfo() {
		return imageInfo;
	}

	public boolean isInterlaced() {
		return deinterlacer != null;
	}

	public Deinterlacer getDeinterlacer() {
		return deinterlacer;
	}

	@Override
	protected void startNewChunk(final int len, final String id, final long offset) {
		updateAndCheckChunkGroup(id);
		super.startNewChunk(len, id, offset);
	}

	@Override
	public void close() {
		if (currentChunkGroup != ChunksList.CHUNK_GROUP_6_END)// this could only happen if forced close
			currentChunkGroup = ChunksList.CHUNK_GROUP_6_END;
		super.close();
	}

	public List<PngChunk> getChunks() {
		return chunksList.getChunks();
	}

	public void setMaxTotalBytesRead(final long maxTotalBytesRead) {
		this.maxTotalBytesRead = maxTotalBytesRead;
	}

	public long getSkipChunkMaxSize() {
		return skipChunkMaxSize;
	}

	public void setSkipChunkMaxSize(final long skipChunkMaxSize) {
		this.skipChunkMaxSize = skipChunkMaxSize;
	}

	public long getMaxBytesMetadata() {
		return maxBytesMetadata;
	}

	public void setMaxBytesMetadata(final long maxBytesMetadata) {
		this.maxBytesMetadata = maxBytesMetadata;
	}

	public long getMaxTotalBytesRead() {
		return maxTotalBytesRead;
	}

	@Override
	protected boolean shouldCheckCrc(final int len, final String id) {
		return checkCrc;
	}

	public boolean isCheckCrc() {
		return checkCrc;
	}

	public void setCheckCrc(final boolean checkCrc) {
		this.checkCrc = checkCrc;
	}

	public boolean isCallbackMode() {
		return callbackMode;
	}

	public Set<String> getChunksToSkip() {
		return chunksToSkip;
	}

	public void setChunkLoadBehaviour(final ChunkLoadBehaviour chunkLoadBehaviour) {
		this.chunkLoadBehaviour = chunkLoadBehaviour;
	}

	public ImageInfo getCurImgInfo() {
		return curImageInfo;
	}

	public void updateCurImgInfo(final ImageInfo iminfo) {
		if (!iminfo.equals(curImageInfo))
			curImageInfo = iminfo;
		if (deinterlacer != null)
			deinterlacer = new Deinterlacer(curImageInfo); // we could reset it, but...
	}

	/**
	 * If true, the chunks with no data (because skipped or because processed
	 * like IDAT-type) are still stored in the
	 * PngChunks list, which might be more informative.
	 * 
	 * Setting this to false saves a few bytes
	 * 
	 * Default: false
	 * 
	 * @param includeNonBufferedChunks
	 */
	public void setIncludeNonBufferedChunks(final boolean includeNonBufferedChunks) {
		this.includeNonBufferedChunks = includeNonBufferedChunks;
	}

}
