package net.dev123.commons;

import java.io.File;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

import net.dev123.commons.http.HttpRequestHelper;
import net.dev123.commons.util.FileUtil;
import net.dev123.exception.LibException;

public class ImageInfo {
	/**
	 * Image formats
	 */
	public enum Format {
		PNG, JPEG, GIF, TIFF, BMP, ICO, WEBP
	}

	private static final int EOI_MARKER = 0xd9;

	private static final int RST_MARKER_START = 0xd0;

	private static final int RST_MARKER_END = 0xd7;

	private static final int TEM_MARKER = 0x01;

	private static final int HUFFMAN_TABLE_MARKER = 0xc4;

	private static final int ARITHMETIC_CODING_CONDITIONING_MARKER = 0xcc;

	private URL url;
	private Format format;
	private byte[] imageData;
	private int width;
	private int height;
	private long size;

	public ImageInfo(URL url) {
		this.url = url;
	}

	public int getWidth() {
		return width;
	}

	public void setWidth(int width) {
		this.width = width;
	}

	public int getHeight() {
		return height;
	}

	public void setHeight(int height) {
		this.height = height;
	}

	public long getSize() {
		return size;
	}

	public String getDisplaySize() {
		return FileUtil.byteCountToDisplaySize(size);
	}

	public void setSize(long size) {
		this.size = size;
	}

	public Format getFormat() {
		return format;
	}

	public void setFormat(Format format) {
		this.format = format;
	}

	public URL getUrl() {
		return url;
	}

	/*
	public void setImageData(byte[] imageData) {
		if (imageData == null) {
			throw new IllegalArgumentException("imageData must not be null");
		}
		if (imageData.length == 0) {
			throw new IllegalArgumentException("imageData must not be empty");
		}
		this.imageData = imageData.clone();
		this.width = -1;
		this.height = -1;
		this.format = null;
	}
	*/

	/**
	 * Updates the dimension fields of the image.
	 *
	 */
	private void updateDimensions() {
		if (imageData == null) {
			throw new UnsupportedOperationException(
					"No image data is available.");
		}
		if (imageData.length < 8) {
			throw new IllegalArgumentException(
					"imageData must be a valid image");
		}
		if (imageData[0] == 'G' && imageData[1] == 'I' && imageData[2] == 'F') {
			updateGifDimensions();
			format = Format.GIF;
		} else if (imageData[0] == (byte) 0x89 && imageData[1] == 'P'
				&& imageData[2] == 'N' && imageData[3] == 'G'
				&& imageData[4] == 0x0d && imageData[5] == 0x0a
				&& imageData[6] == 0x1a && imageData[7] == 0x0a) {
			updatePngDimensions();
			format = Format.PNG;
		} else if (imageData[0] == (byte) 0xff && imageData[1] == (byte) 0xd8) {
			updateJpegDimensions();
			format = Format.JPEG;
		} else if ((imageData[0] == 'I' && imageData[1] == 'I'
				&& imageData[2] == 0x2a && imageData[3] == 0x00)
				|| (imageData[0] == 'M' && imageData[1] == 'M'
						&& imageData[2] == 0x00 && imageData[3] == 0x2a)) {
			updateTiffDimensions();
			format = Format.TIFF;
		} else if (imageData[0] == 'B' && imageData[1] == 'M') {
			updateBmpDimensions();
			format = Format.BMP;
		} else if (imageData[0] == 0x00 && imageData[1] == 0x00
				&& imageData[2] == 0x01 && imageData[3] == 0x00) {
			updateIcoDimensions();
			format = Format.ICO;
		} else if (imageData.length > 16 && imageData[0] == 'R'
				&& imageData[1] == 'I' && imageData[2] == 'F'
				&& imageData[3] == 'F' && imageData[8] == 'W'
				&& imageData[9] == 'E' && imageData[10] == 'B'
				&& imageData[11] == 'P' && imageData[12] == 'V'
				&& imageData[13] == 'P' && imageData[14] == '8') {
			updateWebpDimensions();
			format = Format.WEBP;
		} else {
			throw new IllegalArgumentException(
					"imageData must be a valid image");
		}
	}

	/**
	 * Updates the dimension fields of the GIF image. Based on
	 * http://www.w3.org/Graphics/GIF/spec-gif89a.txt.
	 *
	 */
	private void updateGifDimensions() {
		if (imageData.length < 10) {
			throw new IllegalArgumentException("corrupt GIF format");
		}
		ByteBuffer buffer = ByteBuffer.wrap(imageData);
		buffer.order(ByteOrder.LITTLE_ENDIAN);
		width = buffer.getChar(6) & 0xffff;
		height = buffer.getChar(8) & 0xffff;
	}

	/**
	 * Updates the dimension fields of the PNG image. Based on
	 * http://www.w3.org/TR/2003/REC-PNG-20031110/ sections 5 and 11.2.2.
	 *
	 */
	private void updatePngDimensions() {
		if (imageData.length < 24) {
			throw new IllegalArgumentException("corrupt PNG format");
		}
		ByteBuffer buffer = ByteBuffer.wrap(imageData);
		buffer.order(ByteOrder.BIG_ENDIAN);
		width = buffer.getInt(16);
		height = buffer.getInt(20);
	}

	/**
	 * Updates the dimension fields of the JPEG image. Based on
	 * http://www.w3.org/Graphics/JPEG/itu-t81.pdf.
	 *
	 */
	private void updateJpegDimensions() {

		ByteBuffer buffer = ByteBuffer.wrap(imageData);
		buffer.order(ByteOrder.BIG_ENDIAN);

		if (extend(buffer.get()) != 0xff || extend(buffer.get()) != 0xd8) {
			throw new IllegalArgumentException(
					"corrupt JPEG format: Expected SOI marker");
		}

		int code;

		try {
			while (true) {
				do {
					code = extend(buffer.get());
				} while (code != 0xff);

				while (code == 0xff) {
					code = extend(buffer.get());
				}

				if (isFrameMarker(code)) {
					buffer.position(buffer.position() + 3);
					height = extend(buffer.getShort());
					width = extend(buffer.getShort());
					return;
				}

				if (code == EOI_MARKER) {
					throw new IllegalArgumentException(
							"corrupt JPEG format: No frame sgements found.");
				}

				if (code >= RST_MARKER_START && code <= RST_MARKER_END) {
					continue;
				}

				if (code == TEM_MARKER) {
					continue;
				}

				int length = extend(buffer.getShort(buffer.position()));
				buffer.position(buffer.position() + length);
			}
		} catch (IllegalArgumentException ex) {
			throw new IllegalArgumentException("corrupt JPEG format");
		} catch (java.nio.BufferUnderflowException ex) {
			throw new IllegalArgumentException("corrupt JPEG format");
		}
	}

	private static boolean isFrameMarker(int code) {
		return ((code & 0xf0) == 0xc0) && code != HUFFMAN_TABLE_MARKER
				&& code != ARITHMETIC_CODING_CONDITIONING_MARKER;
	}

	private static int extend(byte b) {
		return b & 0xFF;
	}

	private static int extend(short s) {
		return s & 0xFFFF;
	}

	/**
	 * Updates the dimension fields of the TIFF image. Based on
	 * http://partners.adobe.com/public/developer/en/tiff/TIFF6.pdf sections 2
	 * and 3.
	 *
	 */
	private void updateTiffDimensions() {
		ByteBuffer buffer = ByteBuffer.wrap(imageData);
		if (imageData[0] == 'I') {
			buffer.order(ByteOrder.LITTLE_ENDIAN);
		}
		int offset = buffer.getInt(4);

		int ifdSize = buffer.getChar(offset) & 0xffff;
		offset += 2;
		for (int i = 0; i < ifdSize && offset + 12 <= imageData.length; i++) {
			int tag = buffer.getChar(offset) & 0xffff;
			if (tag == 0x100 || tag == 0x101) {
				int type = buffer.getChar(offset + 2) & 0xffff;
				int result;
				if (type == 3) {
					result = buffer.getChar(offset + 8) & 0xffff;
				} else if (type == 4) {
					result = buffer.getInt(offset + 8);
				} else {
					result = imageData[offset + 8];
				}
				if (tag == 0x100) {
					width = result;
					if (height != -1) {
						return;
					}
				} else {
					height = result;
					if (width != -1) {
						return;
					}
				}
			}
			offset += 12;
		}
		if (width == -1 || height == -1) {
			throw new IllegalArgumentException("corrupt tiff format");
		}
	}

	/**
	 * Updates the dimension fields of the BMP image. Based on
	 * http://msdn.microsoft.com/en-us/library/ms532290(VS.85).aspx
	 * http://msdn.microsoft.com/en-us/library/ms532300(VS.85).aspx
	 * http://msdn.microsoft.com/en-us/library/ms532331(VS.85).aspx for windows
	 * versions.
	 *
	 */
	private void updateBmpDimensions() {
		if (imageData.length < 18) {
			throw new IllegalArgumentException("corrupt BMP format");
		}
		ByteBuffer buffer = ByteBuffer.wrap(imageData);
		buffer.order(ByteOrder.LITTLE_ENDIAN);
		width = buffer.get(6) & 0xff;
		height = buffer.get(7) & 0xff;
		int headerLength = buffer.getInt(14);
		if (headerLength == 12 && imageData.length >= 22) {
			width = buffer.getChar(18) & 0xffff;
			height = buffer.getChar(20) & 0xffff;
		} else if ((headerLength == 40 || headerLength == 108
				|| headerLength == 124 || headerLength == 64)
				&& imageData.length >= 26) {
			width = buffer.getInt(18);
			height = buffer.getInt(22);
		} else {
			throw new IllegalArgumentException("corrupt BMP format");
		}
	}

	/**
	 * Updates the dimension fields of the ICO image.
	 *
	 */
	private void updateIcoDimensions() {
		if (imageData.length < 8) {
			throw new IllegalArgumentException("corrupt ICO format");
		}
		ByteBuffer buffer = ByteBuffer.wrap(imageData);
		buffer.order(ByteOrder.LITTLE_ENDIAN);
		width = buffer.get(6) & 0xff;
		height = buffer.get(7) & 0xff;

		if (width == 0) {
			width = 256;
		}
		if (height == 0) {
			height = 256;
		}
	}

	private void updateWebpDimensions() {
		if (imageData.length < 30) {
			throw new IllegalArgumentException("corrupt WEBP format");
		}

		ByteBuffer buffer = ByteBuffer.wrap(imageData);
		buffer.order(ByteOrder.LITTLE_ENDIAN);

		int bits = buffer.get(20) | buffer.get(21) << 8 | buffer.get(22) << 16;

		boolean keyFrame = (bits & 1) == 0;

		if (!keyFrame) {
			throw new IllegalArgumentException(
					"corrupt WEBP format: not a key frame");
		}

		int profile = (bits >> 1) & 7;
		int showFrame = (bits >> 4) & 1;

		if (profile > 3) {
			throw new IllegalArgumentException(
					"corrupt WEBP format: invalid profile");
		}
		if (showFrame == 0) {
			throw new IllegalArgumentException(
					"corrupt WEBP format: frame is not visible");
		}

		width = extend(buffer.getShort(26));
		height = extend(buffer.getShort(28));

	}

	public static ImageInfo getImageInfo(URL url) throws LibException {
		if (url == null) {
			return null;
		}

		ImageInfo info = null;
		String scheme = url.getProtocol();
		if ("file".equalsIgnoreCase(scheme)) {
			File imageFile = FileUtil.toFile(url);
			info = new ImageInfo(url);
			info.imageData = FileUtil.readFileToByteArray(imageFile);
			info.setSize(FileUtil.sizeOf(imageFile)); // 设置文件大小
			info.updateDimensions(); // 更新图片尺寸、格式信息
		} else if ("http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme)) {
			info = HttpRequestHelper.getImageInfo(url.toString());
		}
		return info;
	}

	public static ImageInfo getImageInfo(File file) throws LibException {
		if (file == null || !file.exists() || !file.isFile()) {
			return null;
		}

		ImageInfo info = null;
		info = new ImageInfo(null);
		info.imageData = FileUtil.readFileToByteArray(file);
		info.setSize(FileUtil.sizeOf(file)); // 设置文件大小
		info.updateDimensions(); // 更新图片尺寸、格式信息

		return info;
	}
	
	@Override
	public String toString() {
		return "ImageInfo [url=" + url + ", format=" + format + ", width="
				+ width + ", height=" + height + ", size=" + getDisplaySize() + "]";
	}

}
