package ar.com.hjg.pngj;

import java.util.zip.Checksum;

/**
 * Simple immutable wrapper for basic image info.
 * <p>
 * Some parameters are redundant, but the constructor receives an 'orthogonal'
 * subset.
 * <p>
 * ref: http://www.w3.org/TR/PNG/#11IHDR
 */
public class ImageInfo {

	/**
	 * Absolute allowed maximum value for rows and cols (2^24 ~16 million).
	 * (bytesPerRow must fit in a 32bit integer,
	 * though total amount of pixels not necessarily).
	 */
	public static final int MAX_COLS_ROW = 16777216;

	/**
	 * Cols= Image width, in pixels.
	 */
	public final int cols;

	/**
	 * Rows= Image height, in pixels
	 */
	public final int rows;

	/**
	 * Bits per sample (per channel) in the buffer (1-2-4-8-16). This is 8-16
	 * for RGB/ARGB images, 1-2-4-8 for grayscale.
	 * For indexed images, number of bits per palette index (1-2-4-8)
	 */
	public final int bitDepth;

	/**
	 * Number of channels, as used internally: 3 for RGB, 4 for RGBA, 2 for GA
	 * (gray with alpha), 1 for grayscale or
	 * indexed.
	 */
	public final int channels;

	/**
	 * Flag: true if has alpha channel (RGBA/GA)
	 */
	public final boolean alpha;

	/**
	 * Flag: true if is grayscale (G/GA)
	 */
	public final boolean greyscale;

	/**
	 * Flag: true if image is indexed, i.e., it has a palette
	 */
	public final boolean indexed;

	/**
	 * Flag: true if image internally uses less than one byte per sample (bit
	 * depth 1-2-4)
	 */
	public final boolean packed;

	/**
	 * Bits used for each pixel in the buffer: channel * bitDepth
	 */
	public final int bitspPixel;

	/**
	 * rounded up value: this is only used internally for filter
	 */
	public final int bytesPixel;

	/**
	 * ceil(bitspp*cols/8) - does not include filter
	 */
	public final int bytesPerRow;

	/**
	 * Equals cols * channels
	 */
	public final int samplesPerRow;

	/**
	 * Amount of "packed samples" : when several samples are stored in a single
	 * byte (bitdepth 1,2 4) they are counted as
	 * one "packed sample". This is less that samplesPerRow only when bitdepth
	 * is 1-2-4 (flag packed = true)
	 * <p>
	 * This equals the number of elements in the scanline array if working with
	 * packedMode=true
	 * <p>
	 * For internal use, client code should rarely access this.
	 */
	public final int samplesPerRowPacked;

	private long totalPixels = -1; // lazy getter

	private long totalRawBytes = -1; // lazy getter

	/**
	 * Short constructor: assumes truecolor (RGB/RGBA)
	 */
	public ImageInfo(final int cols, final int rows, final int bitdepth, final boolean alpha) {
		this(cols, rows, bitdepth, alpha, false, false);
	}

	/**
	 * Full constructor
	 * 
	 * @param cols
	 *            Width in pixels
	 * @param rows
	 *            Height in pixels
	 * @param bitdepth
	 *            Bits per sample, in the buffer : 8-16 for RGB true color and
	 *            greyscale
	 * @param alpha
	 *            Flag: has an alpha channel (RGBA or GA)
	 * @param grayscale
	 *            Flag: is gray scale (any bitdepth, with or without alpha)
	 * @param indexed
	 *            Flag: has palette
	 */
	public ImageInfo(final int cols, final int rows, final int bitdepth, final boolean alpha, final boolean grayscale, final boolean indexed) {
		this.cols = cols;
		this.rows = rows;
		this.alpha = alpha;
		this.indexed = indexed;
		greyscale = grayscale;
		if (greyscale && indexed)
			throw new PngjException("palette and greyscale are mutually exclusive");
		channels = grayscale || indexed ? alpha ? 2 : 1 : alpha ? 4 : 3;
		// http://www.w3.org/TR/PNG/#11IHDR
		bitDepth = bitdepth;
		packed = bitdepth < 8;
		bitspPixel = channels * bitDepth;
		bytesPixel = (bitspPixel + 7) / 8;
		bytesPerRow = (bitspPixel * cols + 7) / 8;
		samplesPerRow = channels * this.cols;
		samplesPerRowPacked = packed ? bytesPerRow : samplesPerRow;
		// several checks
		switch (bitDepth) {
			case 1:
			case 2:
			case 4:
				if (!(this.indexed || greyscale))
					throw new PngjException("only indexed or grayscale can have bitdepth=" + bitDepth);
				break;
			case 8:
				break;
			case 16:
				if (this.indexed)
					throw new PngjException("indexed can't have bitdepth=" + bitDepth);
				break;
			default:
				throw new PngjException("invalid bitdepth=" + bitDepth);
		}
		if (cols < 1 || cols > ImageInfo.MAX_COLS_ROW)
			throw new PngjException("invalid cols=" + cols + " ???");
		if (rows < 1 || rows > ImageInfo.MAX_COLS_ROW)
			throw new PngjException("invalid rows=" + rows + " ???");
		if (samplesPerRow < 1)
			throw new PngjException("invalid image parameters (overflow?)");
	}

	/**
	 * returns a copy with different size
	 * 
	 * @param cols
	 *            if non-positive, the original is used
	 * @param rows
	 *            if non-positive, the original is used
	 * @return a new copy with the specified size and same properties
	 */
	public ImageInfo withSize(final int cols, final int rows) {
		return new ImageInfo(cols > 0 ? cols : this.cols, rows > 0 ? rows : this.rows, bitDepth, alpha, greyscale, indexed);
	}

	public long getTotalPixels() {
		if (totalPixels < 0)
			totalPixels = cols * (long) rows;
		return totalPixels;
	}

	/**
	 * Total uncompressed bytes in IDAT, including filter byte. This is not
	 * valid for interlaced.
	 */
	public long getTotalRawBytes() {
		if (totalRawBytes < 0)
			totalRawBytes = (bytesPerRow + 1) * (long) rows;
		return totalRawBytes;
	}

	@Override
	public String toString() {
		return "ImageInfo [cols=" + cols + ", rows=" + rows + ", bitDepth=" + bitDepth + ", channels=" + channels + ", alpha=" + alpha + ", greyscale=" + greyscale + ", indexed=" + indexed + "]";
	}

	/**
	 * Brief info: COLSxROWS[dBITDEPTH][a][p][g] ( the default dBITDEPTH='d8' is
	 * ommited)
	 **/
	public String toStringBrief() {
		return String.valueOf(cols) + "x" + rows + (bitDepth != 8 ? "d" + bitDepth : "") + (alpha ? "a" : "") + (indexed ? "p" : "") + (greyscale ? "g" : "");
	}

	public String toStringDetail() {
		return "ImageInfo [cols=" + cols + ", rows=" + rows + ", bitDepth=" + bitDepth + ", channels=" + channels + ", bitspPixel=" + bitspPixel + ", bytesPixel=" + bytesPixel + ", bytesPerRow=" + bytesPerRow + ", samplesPerRow=" + samplesPerRow + ", samplesPerRowP=" + samplesPerRowPacked + ", alpha=" + alpha + ", greyscale=" + greyscale + ", indexed=" + indexed + ", packed=" + packed + "]";
	}

	void updateCrc(final Checksum crc) {
		crc.update((byte) rows);
		crc.update((byte) (rows >> 8));
		crc.update((byte) (rows >> 16));
		crc.update((byte) cols);
		crc.update((byte) (cols >> 8));
		crc.update((byte) (cols >> 16));
		crc.update((byte) bitDepth);
		crc.update((byte) (indexed ? 1 : 2));
		crc.update((byte) (greyscale ? 3 : 4));
		crc.update((byte) (alpha ? 3 : 4));
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + (alpha ? 1231 : 1237);
		result = prime * result + bitDepth;
		result = prime * result + cols;
		result = prime * result + (greyscale ? 1231 : 1237);
		result = prime * result + (indexed ? 1231 : 1237);
		result = prime * result + rows;
		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 ImageInfo other = (ImageInfo) obj;
		if (alpha != other.alpha)
			return false;
		if (bitDepth != other.bitDepth)
			return false;
		if (cols != other.cols)
			return false;
		if (greyscale != other.greyscale)
			return false;
		if (indexed != other.indexed)
			return false;
		if (rows != other.rows)
			return false;
		return true;
	}
}
