package jogamp.opengl.util.pngj.chunks;

import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

import jogamp.opengl.util.pngj.ImageInfo;
import jogamp.opengl.util.pngj.PngjExceptionInternal;

/**
 * Represents a instance of a PNG chunk.
 * <p>
 * See <a
 * href="http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html">http://www
 * .libpng.org/pub/png/spec/1.2/PNG-Chunks .html</a> </a>
 * <p>
 * Concrete classes should extend {@link PngChunkSingle} or
 * {@link PngChunkMultiple}
 * <p>
 * Note that some methods/fields are type-specific (getOrderingConstraint(),
 * allowsMultiple()),<br>
 * some are 'almost' type-specific (id,crit,pub,safe; the exception is
 * PngUKNOWN), <br>
 * and the rest are instance-specific
 */
public abstract class PngChunk {

	/**
	 * Chunk-id: 4 letters
	 */
	public final String id;
	/**
	 * Autocomputed at creation time
	 */
	public final boolean crit, pub, safe;

	protected final ImageInfo imgInfo;

	/**
	 * Possible ordering constraint for a PngChunk type -only relevant for
	 * ancillary chunks. Theoretically, there could be more general constraints,
	 * but these cover the constraints for standard chunks.
	 */
	public enum ChunkOrderingConstraint {
		/**
		 * no ordering constraint
		 */
		NONE,
		/**
		 * Must go before PLTE (and hence, also before IDAT)
		 */
		BEFORE_PLTE_AND_IDAT,
		/**
		 * Must go after PLTE but before IDAT
		 */
		AFTER_PLTE_BEFORE_IDAT,
		/**
		 * Must before IDAT (before or after PLTE)
		 */
		BEFORE_IDAT,
		/**
		 * Does not apply
		 */
		NA;

		public boolean mustGoBeforePLTE() {
			return this == BEFORE_PLTE_AND_IDAT;
		}

		public boolean mustGoBeforeIDAT() {
			return this == BEFORE_IDAT || this == BEFORE_PLTE_AND_IDAT || this == AFTER_PLTE_BEFORE_IDAT;
		}

		public boolean mustGoAfterPLTE() {
			return this == AFTER_PLTE_BEFORE_IDAT;
		}
	}

	private boolean priority = false; // For writing. Queued chunks with high priority will be written as soon as
										// possible

	protected int chunkGroup = -1; // chunk group where it was read or writen
	protected int length = -1; // merely informational, for read chunks
	protected long offset = 0; // merely informational, for read chunks

	/**
	 * This static map defines which PngChunk class correspond to which ChunkID
	 * <p>
	 * The client can add other chunks to this map statically, before reading an
	 * image, calling PngChunk.factoryRegister(id,class)
	 */
	private final static Map<String, Class<? extends PngChunk>> factoryMap = new HashMap<String, Class<? extends PngChunk>>();
	static {
		factoryMap.put(ChunkHelper.IDAT, PngChunkIDAT.class);
		factoryMap.put(ChunkHelper.IHDR, PngChunkIHDR.class);
		factoryMap.put(ChunkHelper.PLTE, PngChunkPLTE.class);
		factoryMap.put(ChunkHelper.IEND, PngChunkIEND.class);
		factoryMap.put(ChunkHelper.tEXt, PngChunkTEXT.class);
		factoryMap.put(ChunkHelper.iTXt, PngChunkITXT.class);
		factoryMap.put(ChunkHelper.zTXt, PngChunkZTXT.class);
		factoryMap.put(ChunkHelper.bKGD, PngChunkBKGD.class);
		factoryMap.put(ChunkHelper.gAMA, PngChunkGAMA.class);
		factoryMap.put(ChunkHelper.pHYs, PngChunkPHYS.class);
		factoryMap.put(ChunkHelper.iCCP, PngChunkICCP.class);
		factoryMap.put(ChunkHelper.tIME, PngChunkTIME.class);
		factoryMap.put(ChunkHelper.tRNS, PngChunkTRNS.class);
		factoryMap.put(ChunkHelper.cHRM, PngChunkCHRM.class);
		factoryMap.put(ChunkHelper.sBIT, PngChunkSBIT.class);
		factoryMap.put(ChunkHelper.sRGB, PngChunkSRGB.class);
		factoryMap.put(ChunkHelper.hIST, PngChunkHIST.class);
		factoryMap.put(ChunkHelper.sPLT, PngChunkSPLT.class);
		// extended
		factoryMap.put(PngChunkOFFS.ID, PngChunkOFFS.class);
		factoryMap.put(PngChunkSTER.ID, PngChunkSTER.class);
	}

	/**
	 * Registers a chunk-id (4 letters) to be associated with a PngChunk class
	 * <p>
	 * This method should be called by user code that wants to add some chunks
	 * (not implmemented in this library) to the factory, so that the PngReader
	 * knows about it.
	 */
	public static void factoryRegister(final String chunkId, final Class<? extends PngChunk> chunkClass) {
		factoryMap.put(chunkId, chunkClass);
	}

	/**
	 * True if the chunk-id type is known.
	 * <p>
	 * A chunk is known if we recognize its class, according with
	 * <code>factoryMap</code>
	 * <p>
	 * This is not necessarily the same as being "STANDARD", or being
	 * implemented in this library
	 * <p>
	 * Unknown chunks will be parsed as instances of {@link PngChunkUNKNOWN}
	 */
	public static boolean isKnown(final String id) {
		return factoryMap.containsKey(id);
	}

	protected PngChunk(final String id, final ImageInfo imgInfo) {
		this.id = id;
		this.imgInfo = imgInfo;
		this.crit = ChunkHelper.isCritical(id);
		this.pub = ChunkHelper.isPublic(id);
		this.safe = ChunkHelper.isSafeToCopy(id);
	}

	/**
	 * This factory creates the corresponding chunk and parses the raw chunk.
	 * This is used when reading.
	 */
	public static PngChunk factory(final ChunkRaw chunk, final ImageInfo info) {
		final PngChunk c = factoryFromId(ChunkHelper.toString(chunk.idbytes), info);
		c.length = chunk.len;
		c.parseFromRaw(chunk);
		return c;
	}

	/**
	 * Creates one new blank chunk of the corresponding type, according to
	 * factoryMap (PngChunkUNKNOWN if not known)
	 */
	public static PngChunk factoryFromId(final String cid, final ImageInfo info) {
		PngChunk chunk = null;
		try {
			final Class<? extends PngChunk> cla = factoryMap.get(cid);
			if (cla != null) {
				final Constructor<? extends PngChunk> constr = cla.getConstructor(ImageInfo.class);
				chunk = constr.newInstance(info);
			}
		} catch (final Exception e) {
			// this can happen for unkown chunks
		}
		if (chunk == null)
			chunk = new PngChunkUNKNOWN(cid, info);
		return chunk;
	}

	protected final ChunkRaw createEmptyChunk(final int len, final boolean alloc) {
		final ChunkRaw c = new ChunkRaw(len, ChunkHelper.toBytes(id), alloc);
		return c;
	}

	/**
	 * Makes a clone (deep copy) calling {@link #cloneDataFromRead(PngChunk)}
	 */
	@SuppressWarnings("unchecked")
	public static <T extends PngChunk> T cloneChunk(final T chunk, final ImageInfo info) {
		final PngChunk cn = factoryFromId(chunk.id, info);
		if (cn.getClass() != chunk.getClass())
			throw new PngjExceptionInternal("bad class cloning chunk: " + cn.getClass() + " " + chunk.getClass());
		cn.cloneDataFromRead(chunk);
		return (T) cn;
	}

	/**
	 * In which "chunkGroup" (see {@link ChunksList}for definition) this chunks
	 * instance was read or written.
	 * <p>
	 * -1 if not read or written (eg, queued)
	 */
	final public int getChunkGroup() {
		return chunkGroup;
	}

	/**
	 * @see #getChunkGroup()
	 */
	final public void setChunkGroup(final int chunkGroup) {
		this.chunkGroup = chunkGroup;
	}

	public boolean hasPriority() {
		return priority;
	}

	public void setPriority(final boolean priority) {
		this.priority = priority;
	}

	final void write(final OutputStream os) {
		final ChunkRaw c = createRawChunk();
		if (c == null)
			throw new PngjExceptionInternal("null chunk ! creation failed for " + this);
		c.writeChunk(os);
	}

	public int getLength() {
		return length;
	}

	/*
	 * public void setLength(int length) { this.length = length; }
	 */

	public long getOffset() {
		return offset;
	}

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

	/**
	 * Creates the physical chunk. This is used when writing (serialization).
	 * Each particular chunk class implements its own logic.
	 *
	 * @return A newly allocated and filled raw chunk
	 */
	public abstract ChunkRaw createRawChunk();

	/**
	 * Parses raw chunk and fill inside data. This is used when reading
	 * (deserialization). Each particular chunk class implements its own logic.
	 */
	public abstract void parseFromRaw(ChunkRaw c);

	/**
	 * Makes a copy of the chunk.
	 * <p>
	 * This is used when copying chunks from a reader to a writer
	 * <p>
	 * It should normally be a deep copy, and after the cloning
	 * this.equals(other) should return true
	 */
	public abstract void cloneDataFromRead(PngChunk other);

	public abstract boolean allowsMultiple(); // this is implemented in PngChunkMultiple/PngChunSingle

	/**
	 * see {@link ChunkOrderingConstraint}
	 */
	public abstract ChunkOrderingConstraint getOrderingConstraint();

	@Override
	public String toString() {
		return "chunk id= " + id + " (len=" + length + " offset=" + offset + ") c=" + getClass().getSimpleName();
	}

}
