package ar.com.hjg.pngj.chunks;

// see http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html
// http://www.w3.org/TR/PNG/#5Chunk-naming-conventions
// http://www.w3.org/TR/PNG/#table53
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;

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

/**
 * Helper methods and constants related to Chunk processing.
 * <p>
 * This should only be of interest to developers doing special chunk processing
 * or extending the ChunkFactory
 */
public class ChunkHelper {
	ChunkHelper() {}

	public static final String IHDR = "IHDR";
	public static final String PLTE = "PLTE";
	public static final String IDAT = "IDAT";
	public static final String IEND = "IEND";
	public static final String cHRM = "cHRM";
	public static final String gAMA = "gAMA";
	public static final String iCCP = "iCCP";
	public static final String sBIT = "sBIT";
	public static final String sRGB = "sRGB";
	public static final String bKGD = "bKGD";
	public static final String hIST = "hIST";
	public static final String tRNS = "tRNS";
	public static final String pHYs = "pHYs";
	public static final String sPLT = "sPLT";
	public static final String tIME = "tIME";
	public static final String iTXt = "iTXt";
	public static final String tEXt = "tEXt";
	public static final String zTXt = "zTXt";

	public static final byte[] b_IHDR = ChunkHelper.toBytes(ChunkHelper.IHDR);
	public static final byte[] b_PLTE = ChunkHelper.toBytes(ChunkHelper.PLTE);
	public static final byte[] b_IDAT = ChunkHelper.toBytes(ChunkHelper.IDAT);
	public static final byte[] b_IEND = ChunkHelper.toBytes(ChunkHelper.IEND);

	/*
	 * static auxiliary buffer. any method that uses this should synchronize against this
	 */
	private static byte[] tmpbuffer = new byte[4096];

	/**
	 * Converts to bytes using Latin1 (ISO-8859-1)
	 */
	public static byte[] toBytes(final String x) {
		try {
			return x.getBytes(PngHelperInternal.charsetLatin1name);
		} catch (final UnsupportedEncodingException e) {
			throw new PngBadCharsetException(e);
		}
	}

	/**
	 * Converts to String using Latin1 (ISO-8859-1)
	 */
	public static String toString(final byte[] x) {
		try {
			return new String(x, PngHelperInternal.charsetLatin1name);
		} catch (final UnsupportedEncodingException e) {
			throw new PngBadCharsetException(e);
		}
	}

	/**
	 * Converts to String using Latin1 (ISO-8859-1)
	 */
	public static String toString(final byte[] x, final int offset, final int len) {
		try {
			return new String(x, offset, len, PngHelperInternal.charsetLatin1name);
		} catch (final UnsupportedEncodingException e) {
			throw new PngBadCharsetException(e);
		}
	}

	/**
	 * Converts to bytes using UTF-8
	 */
	public static byte[] toBytesUTF8(final String x) {
		try {
			return x.getBytes(PngHelperInternal.charsetUTF8name);
		} catch (final UnsupportedEncodingException e) {
			throw new PngBadCharsetException(e);
		}
	}

	/**
	 * Converts to string using UTF-8
	 */
	public static String toStringUTF8(final byte[] x) {
		try {
			return new String(x, PngHelperInternal.charsetUTF8name);
		} catch (final UnsupportedEncodingException e) {
			throw new PngBadCharsetException(e);
		}
	}

	/**
	 * Converts to string using UTF-8
	 */
	public static String toStringUTF8(final byte[] x, final int offset, final int len) {
		try {
			return new String(x, offset, len, PngHelperInternal.charsetUTF8name);
		} catch (final UnsupportedEncodingException e) {
			throw new PngBadCharsetException(e);
		}
	}

	/**
	 * critical chunk : first letter is uppercase
	 */
	public static boolean isCritical(final String id) {
		return Character.isUpperCase(id.charAt(0));
	}

	/**
	 * public chunk: second letter is uppercase
	 */
	public static boolean isPublic(final String id) { //
		return Character.isUpperCase(id.charAt(1));
	}

	/**
	 * Safe to copy chunk: fourth letter is lower case
	 */
	public static boolean isSafeToCopy(final String id) {
		return !Character.isUpperCase(id.charAt(3));
	}

	/**
	 * "Unknown" just means that our chunk factory (even when it has been
	 * augmented by client code) did not recognize its
	 * id
	 */
	public static boolean isUnknown(final PngChunk c) {
		return c instanceof PngChunkUNKNOWN;
	}

	/**
	 * Finds position of null byte in array
	 * 
	 * @param b
	 * @return -1 if not found
	 */
	public static int posNullByte(final byte[] b) {
		for (int i = 0; i < b.length; i++)
			if (b[i] == 0)
				return i;
		return -1;
	}

	/**
	 * Decides if a chunk should be loaded, according to a ChunkLoadBehaviour
	 * 
	 * @param id
	 * @param behav
	 * @return true/false
	 */
	public static boolean shouldLoad(final String id, final ChunkLoadBehaviour behav) {
		if (ChunkHelper.isCritical(id))
			return true;
		switch (behav) {
			case LOAD_CHUNK_ALWAYS:
				return true;
			case LOAD_CHUNK_IF_SAFE:
				return ChunkHelper.isSafeToCopy(id);
			case LOAD_CHUNK_NEVER:
				return false;
			case LOAD_CHUNK_MOST_IMPORTANT:
				return id.equals(PngChunkTRNS.ID);
		}
		return false; // should not reach here
	}

	public final static byte[] compressBytes(final byte[] ori, final boolean compress) {
		return ChunkHelper.compressBytes(ori, 0, ori.length, compress);
	}

	public static byte[] compressBytes(final byte[] ori, final int offset, final int len, final boolean compress) {
		try {
			final ByteArrayInputStream inb = new ByteArrayInputStream(ori, offset, len);
			final InputStream in = compress ? inb : new InflaterInputStream(inb);
			final ByteArrayOutputStream outb = new ByteArrayOutputStream();
			final OutputStream out = compress ? new DeflaterOutputStream(outb) : outb;
			ChunkHelper.shovelInToOut(in, out);
			in.close();
			out.close();
			return outb.toByteArray();
		} catch (final Exception e) {
			throw new PngjException(e);
		}
	}

	/**
	 * Shovels all data from an input stream to an output stream.
	 */
	private static void shovelInToOut(final InputStream in, final OutputStream out) throws IOException {
		synchronized (ChunkHelper.tmpbuffer) {
			int len;
			while ((len = in.read(ChunkHelper.tmpbuffer)) > 0)
				out.write(ChunkHelper.tmpbuffer, 0, len);
		}
	}

	/**
	 * Returns only the chunks that "match" the predicate
	 * 
	 * See also trimList()
	 */
	public static List<PngChunk> filterList(final List<PngChunk> target, final ChunkPredicate predicateKeep) {
		final List<PngChunk> result = new ArrayList<>();
		for (final PngChunk element : target)
			if (predicateKeep.match(element))
				result.add(element);
		return result;
	}

	/**
	 * Remove (in place) the chunks that "match" the predicate
	 * 
	 * See also filterList
	 */
	public static int trimList(final List<PngChunk> target, final ChunkPredicate predicateRemove) {
		final Iterator<PngChunk> it = target.iterator();
		int cont = 0;
		while (it.hasNext()) {
			final PngChunk c = it.next();
			if (predicateRemove.match(c)) {
				it.remove();
				cont++;
			}
		}
		return cont;
	}

	/**
	 * Adhoc criteria: two ancillary chunks are "equivalent" ("practically same
	 * type") if they have same id and (perhaps,
	 * if multiple are allowed) if the match also in some "internal key" (eg:
	 * key for string values, palette for sPLT,
	 * etc)
	 * 
	 * When we use this method, we implicitly assume that we don't allow/expect
	 * two "equivalent" chunks in a single PNG
	 * 
	 * Notice that the use of this is optional, and that the PNG standard
	 * actually allows text chunks that have same key
	 * 
	 * @return true if "equivalent"
	 */
	public static final boolean equivalent(final PngChunk c1, final PngChunk c2) {
		if (c1 == c2)
			return true;
		if (c1 == null || c2 == null || !c1.id.equals(c2.id))
			return false;
		if (c1.crit)
			return false;
		// same id
		if (c1.getClass() != c2.getClass())
			return false; // should not happen
		if (!c2.allowsMultiple())
			return true;
		if (c1 instanceof PngChunkTextVar)
			return ((PngChunkTextVar) c1).getKey().equals(((PngChunkTextVar) c2).getKey());
		if (c1 instanceof PngChunkSPLT)
			return ((PngChunkSPLT) c1).getPalName().equals(((PngChunkSPLT) c2).getPalName());
		// unknown chunks that allow multiple? consider they don't match
		return false;
	}

	public static boolean isText(final PngChunk c) {
		return c instanceof PngChunkTextVar;
	}

}
