package jogamp.opengl.util.pngj;

import jogamp.opengl.util.pngj.ImageLine.SampleType;
import jogamp.opengl.util.pngj.chunks.PngChunkPLTE;
import jogamp.opengl.util.pngj.chunks.PngChunkTRNS;

/**
 * Bunch of utility static methods to process/analyze an image line at the pixel
 * level.
 * <p>
 * Not essential at all, some methods are probably to be removed if future
 * releases.
 * <p>
 * WARNING: most methods for getting/setting values work currently only for
 * integer base imageLines
 */
public class ImageLineHelper {

	private final static double BIG_VALUE = Double.MAX_VALUE * 0.5;

	private final static double BIG_VALUE_NEG = Double.MAX_VALUE * (-0.5);

	/**
	 * Given an indexed line with a palette, unpacks as a RGB array, or RGBA if
	 * a non nul PngChunkTRNS chunk is passed
	 *
	 * @param line
	 *            ImageLine as returned from PngReader
	 * @param pal
	 *            Palette chunk
	 * @param buf
	 *            Preallocated array, optional
	 * @return R G B (A), one sample 0-255 per array element. Ready for
	 *         pngw.writeRowInt()
	 */
	public static int[] palette2rgb(ImageLine line, final PngChunkPLTE pal, final PngChunkTRNS trns, int[] buf) {
		final boolean isalpha = trns != null;
		final int channels = isalpha ? 4 : 3;
		final int nsamples = line.imgInfo.cols * channels;
		if (buf == null || buf.length < nsamples)
			buf = new int[nsamples];
		if (!line.samplesUnpacked)
			line = line.unpackToNewImageLine();
		final boolean isbyte = line.sampleType == SampleType.BYTE;
		final int nindexesWithAlpha = trns != null ? trns.getPalletteAlpha().length : 0;
		for (int c = 0; c < line.imgInfo.cols; c++) {
			final int index = isbyte ? (line.scanlineb[c] & 0xFF) : line.scanline[c];
			pal.getEntryRgb(index, buf, c * channels);
			if (isalpha) {
				final int alpha = index < nindexesWithAlpha ? trns.getPalletteAlpha()[index] : 255;
				buf[c * channels + 3] = alpha;
			}
		}
		return buf;
	}

	public static int[] palette2rgb(final ImageLine line, final PngChunkPLTE pal, final int[] buf) {
		return palette2rgb(line, pal, null, buf);
	}

	/**
	 * what follows is pretty uninteresting/untested/obsolete, subject to change
	 */
	/**
	 * Just for basic info or debugging. Shows values for first and last pixel.
	 * Does not include alpha
	 */
	public static String infoFirstLastPixels(final ImageLine line) {
		return line.imgInfo.channels == 1 ? String.format("first=(%d) last=(%d)", line.scanline[0],
				line.scanline[line.scanline.length - 1]) : String.format("first=(%d %d %d) last=(%d %d %d)",
				line.scanline[0], line.scanline[1], line.scanline[2], line.scanline[line.scanline.length
						- line.imgInfo.channels], line.scanline[line.scanline.length - line.imgInfo.channels + 1],
				line.scanline[line.scanline.length - line.imgInfo.channels + 2]);
	}

	public static String infoFull(final ImageLine line) {
		final ImageLineStats stats = new ImageLineStats(line);
		return "row=" + line.getRown() + " " + stats.toString() + "\n  " + infoFirstLastPixels(line);
	}

	/**
	 * Computes some statistics for the line. Not very efficient or elegant,
	 * mainly for tests. Only for RGB/RGBA Outputs values as doubles (0.0 - 1.0)
	 */
	static class ImageLineStats {
		public double[] prom = { 0.0, 0.0, 0.0, 0.0 }; // channel averages
		public double[] maxv = { BIG_VALUE_NEG, BIG_VALUE_NEG, BIG_VALUE_NEG, BIG_VALUE_NEG }; // maximo
		public double[] minv = { BIG_VALUE, BIG_VALUE, BIG_VALUE, BIG_VALUE };
		public double promlum = 0.0; // maximum global (luminance)
		public double maxlum = BIG_VALUE_NEG; // max luminance
		public double minlum = BIG_VALUE;
		public double[] maxdif = { BIG_VALUE_NEG, BIG_VALUE_NEG, BIG_VALUE_NEG, BIG_VALUE }; // maxima
		public final int channels; // diferencia

		@Override
		public String toString() {
			return channels == 3 ? String.format(
					"prom=%.1f (%.1f %.1f %.1f) max=%.1f (%.1f %.1f %.1f) min=%.1f (%.1f %.1f %.1f)", promlum, prom[0],
					prom[1], prom[2], maxlum, maxv[0], maxv[1], maxv[2], minlum, minv[0], minv[1], minv[2])
					+ String.format(" maxdif=(%.1f %.1f %.1f)", maxdif[0], maxdif[1], maxdif[2]) : String.format(
					"prom=%.1f (%.1f %.1f %.1f %.1f) max=%.1f (%.1f %.1f %.1f %.1f) min=%.1f (%.1f %.1f %.1f %.1f)",
					promlum, prom[0], prom[1], prom[2], prom[3], maxlum, maxv[0], maxv[1], maxv[2], maxv[3], minlum,
					minv[0], minv[1], minv[2], minv[3])
					+ String.format(" maxdif=(%.1f %.1f %.1f %.1f)", maxdif[0], maxdif[1], maxdif[2], maxdif[3]);
		}

		public ImageLineStats(final ImageLine line) {
			this.channels = line.channels;
			if (line.channels < 3)
				throw new PngjException("ImageLineStats only works for RGB - RGBA");
			int ch = 0;
			double lum, x, d;
			for (int i = 0; i < line.imgInfo.cols; i++) {
				lum = 0;
				for (ch = channels - 1; ch >= 0; ch--) {
					x = int2double(line, line.scanline[i * channels]);
					if (ch < 3)
						lum += x;
					prom[ch] += x;
					if (x > maxv[ch])
						maxv[ch] = x;
					if (x < minv[ch])
						minv[ch] = x;
					if (i >= channels) {
						d = Math.abs(x - int2double(line, line.scanline[i - channels]));
						if (d > maxdif[ch])
							maxdif[ch] = d;
					}
				}
				promlum += lum;
				if (lum > maxlum)
					maxlum = lum;
				if (lum < minlum)
					minlum = lum;
			}
			for (ch = 0; ch < channels; ch++) {
				prom[ch] /= line.imgInfo.cols;
			}
			promlum /= (line.imgInfo.cols * 3.0);
			maxlum /= 3.0;
			minlum /= 3.0;
		}
	}

	/**
	 * integer packed R G B only for bitdepth=8! (does not check!)
	 *
	 **/
	public static int getPixelRGB8(final ImageLine line, final int column) {
		final int offset = column * line.channels;
		return (line.scanline[offset] << 16) + (line.scanline[offset + 1] << 8) + (line.scanline[offset + 2]);
	}

	public static int getPixelARGB8(final ImageLine line, final int column) {
		final int offset = column * line.channels;
		return (line.scanline[offset + 3] << 24) + (line.scanline[offset] << 16) + (line.scanline[offset + 1] << 8)
				+ (line.scanline[offset + 2]);
	}

	public static void setPixelsRGB8(final ImageLine line, final int[] rgb) {
		for (int i = 0, j = 0; i < line.imgInfo.cols; i++) {
			line.scanline[j++] = ((rgb[i] >> 16) & 0xFF);
			line.scanline[j++] = ((rgb[i] >> 8) & 0xFF);
			line.scanline[j++] = ((rgb[i] & 0xFF));
		}
	}

	public static void setPixelRGB8(final ImageLine line, int col, final int r, final int g, final int b) {
		col *= line.channels;
		line.scanline[col++] = r;
		line.scanline[col++] = g;
		line.scanline[col] = b;
	}

	public static void setPixelRGB8(final ImageLine line, final int col, final int rgb) {
		setPixelRGB8(line, col, (rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF);
	}

	public static void setPixelsRGBA8(final ImageLine line, final int[] rgb) {
		for (int i = 0, j = 0; i < line.imgInfo.cols; i++) {
			line.scanline[j++] = ((rgb[i] >> 16) & 0xFF);
			line.scanline[j++] = ((rgb[i] >> 8) & 0xFF);
			line.scanline[j++] = ((rgb[i] & 0xFF));
			line.scanline[j++] = ((rgb[i] >> 24) & 0xFF);
		}
	}

	public static void setPixelRGBA8(final ImageLine line, int col, final int r, final int g, final int b, final int a) {
		col *= line.channels;
		line.scanline[col++] = r;
		line.scanline[col++] = g;
		line.scanline[col++] = b;
		line.scanline[col] = a;
	}

	public static void setPixelRGBA8(final ImageLine line, final int col, final int rgb) {
		setPixelRGBA8(line, col, (rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF, (rgb >> 24) & 0xFF);
	}

	public static void setValD(final ImageLine line, final int i, final double d) {
		line.scanline[i] = double2int(line, d);
	}

	public static int interpol(final int a, final int b, final int c, final int d, final double dx, final double dy) {
		// a b -> x (0-1)
		// c d
		//
		final double e = a * (1.0 - dx) + b * dx;
		final double f = c * (1.0 - dx) + d * dx;
		return (int) (e * (1 - dy) + f * dy + 0.5);
	}

	public static double int2double(final ImageLine line, final int p) {
		return line.bitDepth == 16 ? p / 65535.0 : p / 255.0;
		// TODO: replace my multiplication? check for other bitdepths
	}

	public static double int2doubleClamped(final ImageLine line, final int p) {
		// TODO: replace my multiplication?
		final double d = line.bitDepth == 16 ? p / 65535.0 : p / 255.0;
		return d <= 0.0 ? 0 : (d >= 1.0 ? 1.0 : d);
	}

	public static int double2int(final ImageLine line, double d) {
		d = d <= 0.0 ? 0 : (d >= 1.0 ? 1.0 : d);
		return line.bitDepth == 16 ? (int) (d * 65535.0 + 0.5) : (int) (d * 255.0 + 0.5); //
	}

	public static int double2intClamped(final ImageLine line, double d) {
		d = d <= 0.0 ? 0 : (d >= 1.0 ? 1.0 : d);
		return line.bitDepth == 16 ? (int) (d * 65535.0 + 0.5) : (int) (d * 255.0 + 0.5); //
	}

	public static int clampTo_0_255(final int i) {
		return i > 255 ? 255 : (i < 0 ? 0 : i);
	}

	public static int clampTo_0_65535(final int i) {
		return i > 65535 ? 65535 : (i < 0 ? 0 : i);
	}

	public static int clampTo_128_127(final int x) {
		return x > 127 ? 127 : (x < -128 ? -128 : x);
	}

	/**
	 * Unpacks scanline (for bitdepth 1-2-4) into a array <code>int[]</code>
	 * <p>
	 * You can (OPTIONALLY) pass an preallocated array, that will be filled and
	 * returned. If null, it will be allocated
	 * <p>
	 * If
	 * <code>scale==true<code>, it scales the value (just a bit shift) towards 0-255.
	 * <p>
	 * You probably should use {@link ImageLine#unpackToNewImageLine()}
	 *
	 */
	public static int[] unpack(final ImageInfo imgInfo, final int[] src, int[] dst, final boolean scale) {
		final int len1 = imgInfo.samplesPerRow;
		final int len0 = imgInfo.samplesPerRowPacked;
		if (dst == null || dst.length < len1)
			dst = new int[len1];
		if (imgInfo.packed)
			ImageLine.unpackInplaceInt(imgInfo, src, dst, scale);
		else
			System.arraycopy(src, 0, dst, 0, len0);
		return dst;
	}

	public static byte[] unpack(final ImageInfo imgInfo, final byte[] src, byte[] dst, final boolean scale) {
		final int len1 = imgInfo.samplesPerRow;
		final int len0 = imgInfo.samplesPerRowPacked;
		if (dst == null || dst.length < len1)
			dst = new byte[len1];
		if (imgInfo.packed)
			ImageLine.unpackInplaceByte(imgInfo, src, dst, scale);
		else
			System.arraycopy(src, 0, dst, 0, len0);
		return dst;
	}

	/**
	 * Packs scanline (for bitdepth 1-2-4) from array into the scanline
	 * <p>
	 * If <code>scale==true<code>, it scales the value (just a bit shift).
	 *
	 * You probably should use {@link ImageLine#packToNewImageLine()}
	 */
	public static int[] pack(final ImageInfo imgInfo, final int[] src, int[] dst, final boolean scale) {
		final int len0 = imgInfo.samplesPerRowPacked;
		if (dst == null || dst.length < len0)
			dst = new int[len0];
		if (imgInfo.packed)
			ImageLine.packInplaceInt(imgInfo, src, dst, scale);
		else
			System.arraycopy(src, 0, dst, 0, len0);
		return dst;
	}

	public static byte[] pack(final ImageInfo imgInfo, final byte[] src, byte[] dst, final boolean scale) {
		final int len0 = imgInfo.samplesPerRowPacked;
		if (dst == null || dst.length < len0)
			dst = new byte[len0];
		if (imgInfo.packed)
			ImageLine.packInplaceByte(imgInfo, src, dst, scale);
		else
			System.arraycopy(src, 0, dst, 0, len0);
		return dst;
	}

	static int getMaskForPackedFormats(final int bitDepth) { // Utility function for pack/unpack
		if (bitDepth == 4)
			return 0xf0;
		else if (bitDepth == 2)
			return 0xc0;
		else
			return 0x80; // bitDepth == 1
	}

	static int getMaskForPackedFormatsLs(final int bitDepth) { // Utility function for pack/unpack
		if (bitDepth == 4)
			return 0x0f;
		else if (bitDepth == 2)
			return 0x03;
		else
			return 0x01; // bitDepth == 1
	}

}
