package cn.hutool.core.img;

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.color.ColorSpace;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.awt.image.ColorConvertOp;
import java.awt.image.CropImageFilter;
import java.awt.image.FilteredImageSource;
import java.awt.image.ImageFilter;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.net.URL;
import java.nio.file.Path;

import javax.imageio.ImageIO;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.resource.Resource;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;

/**
 * 图像编辑器
 * 
 * @author looly
 * @since 4.1.5
 */
public class Img implements Serializable{
	private static final long serialVersionUID = 1L;

	private BufferedImage srcImage;
	private Image targetImage;
	/** 目标图片文件格式，用于写出 */
	private String targetImageType = ImgUtil.IMAGE_TYPE_JPG;
	/** 计算x,y坐标的时候是否从中心做为原始坐标开始计算 */
	private boolean positionBaseCentre = true;
	/** 图片输出质量，用于压缩 */
	private float quality = -1;

	/**
	 * 从Path读取图片并开始处理
	 * 
	 * @param imagePath 图片文件路径
	 * @return {@link Img}
	 */
	public static Img from(Path imagePath) {
		return from(imagePath.toFile());
	}

	/**
	 * 从文件读取图片并开始处理
	 * 
	 * @param imageFile 图片文件
	 * @return {@link Img}
	 */
	public static Img from(File imageFile) {
		return new Img(ImgUtil.read(imageFile));
	}

	/**
	 * 从资源对象中读取图片并开始处理
	 * 
	 * @param resource 图片资源对象
	 * @return {@link Img}
	 * @since 4.4.1
	 */
	public static Img from(Resource resource) {
		return from(resource.getStream());
	}

	/**
	 * 从流读取图片并开始处理
	 * 
	 * @param in 图片流
	 * @return {@link Img}
	 */
	public static Img from(InputStream in) {
		return new Img(ImgUtil.read(in));
	}

	/**
	 * 从ImageInputStream取图片并开始处理
	 * 
	 * @param imageStream 图片流
	 * @return {@link Img}
	 */
	public static Img from(ImageInputStream imageStream) {
		return new Img(ImgUtil.read(imageStream));
	}

	/**
	 * 从URL取图片并开始处理
	 * 
	 * @param imageUrl 图片URL
	 * @return {@link Img}
	 */
	public static Img from(URL imageUrl) {
		return new Img(ImgUtil.read(imageUrl));
	}

	/**
	 * 从Image取图片并开始处理
	 * 
	 * @param image 图片
	 * @return {@link Img}
	 */
	public static Img from(Image image) {
		return new Img(ImgUtil.toBufferedImage(image));
	}

	/**
	 * 构造
	 * 
	 * @param srcImage 来源图片
	 */
	public Img(BufferedImage srcImage) {
		this.srcImage = srcImage;
	}

	/**
	 * 设置目标图片文件格式，用于写出
	 * 
	 * @param imgType 图片格式
	 * @return this
	 * @see ImgUtil#IMAGE_TYPE_JPG
	 * @see ImgUtil#IMAGE_TYPE_PNG
	 */
	public Img setTargetImageType(String imgType) {
		this.targetImageType = imgType;
		return this;
	}

	/**
	 * 计算x,y坐标的时候是否从中心做为原始坐标开始计算
	 * 
	 * @param positionBaseCentre 是否从中心做为原始坐标开始计算
	 * @since 4.1.15
	 */
	public Img setPositionBaseCentre(boolean positionBaseCentre) {
		this.positionBaseCentre = positionBaseCentre;
		return this;
	}

	/**
	 * 设置图片输出质量，数字为0~1（不包括0和1）表示质量压缩比，除此数字外设置表示不压缩
	 * 
	 * @param quality 质量，数字为0~1（不包括0和1）表示质量压缩比，除此数字外设置表示不压缩
	 * @since 4.3.2
	 */
	public Img setQuality(double quality) {
		return setQuality((float) quality);
	}

	/**
	 * 设置图片输出质量，数字为0~1（不包括0和1）表示质量压缩比，除此数字外设置表示不压缩
	 * 
	 * @param quality 质量，数字为0~1（不包括0和1）表示质量压缩比，除此数字外设置表示不压缩
	 * @since 4.3.2
	 */
	public Img setQuality(float quality) {
		if (quality > 0 && quality < 1) {
			this.quality = quality;
		} else {
			this.quality = 1;
		}
		return this;
	}

	/**
	 * 缩放图像（按比例缩放）
	 * 
	 * @param scale 缩放比例。比例大于1时为放大，小于1大于0为缩小
	 * @return this
	 */
	public Img scale(float scale) {
		if (scale < 0) {
			// 自动修正负数
			scale = -scale;
		}

		final Image srcImg = getValidSrcImg();

		// PNG图片特殊处理
		if (ImgUtil.IMAGE_TYPE_PNG.equals(this.targetImageType)) {
			final AffineTransformOp op = new AffineTransformOp(AffineTransform.getScaleInstance((double) scale, (double) scale), null);
			this.targetImage = op.filter(ImgUtil.toBufferedImage(srcImg), null);
		} else {
			final String scaleStr = Float.toString(scale);
			// 缩放后的图片宽
			int width = NumberUtil.mul(Integer.toString(srcImg.getWidth(null)), scaleStr).intValue();
			// 缩放后的图片高
			int height = NumberUtil.mul(Integer.toString(srcImg.getHeight(null)), scaleStr).intValue();
			scale(width, height);
		}
		return this;
	}

	/**
	 * 缩放图像（按长宽缩放）<br>
	 * 注意：目标长宽与原图不成比例会变形
	 * 
	 * @param width 目标宽度
	 * @param height 目标高度
	 * @return this
	 */
	public Img scale(int width, int height) {
		final Image srcImg = getValidSrcImg();

		int srcHeight = srcImg.getHeight(null);
		int srcWidth = srcImg.getWidth(null);
		int scaleType;
		if (srcHeight == height && srcWidth == width) {
			// 源与目标长宽一致返回原图
			this.targetImage = srcImg;
			return this;
		} else if (srcHeight < height || srcWidth < width) {
			// 放大图片使用平滑模式
			scaleType = Image.SCALE_SMOOTH;
		} else {
			scaleType = Image.SCALE_DEFAULT;
		}
		
		double sx = NumberUtil.div(width, srcWidth);
		double sy = NumberUtil.div(height, srcHeight);

		if (ImgUtil.IMAGE_TYPE_PNG.equals(this.targetImageType)) {
			final AffineTransformOp op = new AffineTransformOp(AffineTransform.getScaleInstance(sx, sy), null);
			this.targetImage = op.filter(ImgUtil.toBufferedImage(srcImg), null);
		} else {
			this.targetImage = srcImg.getScaledInstance(width, height, scaleType);
		}

		return this;
	}

	/**
	 * 等比缩放图像，此方法按照按照给定的长宽等比缩放图片，按照长宽缩放比最多的一边等比缩放，空白部分填充背景色<br>
	 * 缩放后默认为jpeg格式
	 * 
	 * @param width 缩放后的宽度
	 * @param height 缩放后的高度
	 * @param fixedColor 比例不对时补充的颜色，不补充为<code>null</code>
	 * @return this
	 */
	public Img scale(int width, int height, Color fixedColor) {
		Image srcImage = getValidSrcImg();
		int srcHeight = srcImage.getHeight(null);
		int srcWidth = srcImage.getWidth(null);
		double heightRatio = NumberUtil.div(height, srcHeight);
		double widthRatio = NumberUtil.div(width, srcWidth);
		if (heightRatio == widthRatio) {
			// 长宽都按照相同比例缩放时，返回缩放后的图片
			return scale(width, height);
		}

		// 宽缩放比例多就按照宽缩放，否则按照高缩放
		if (widthRatio < heightRatio) {
			scale(width, (int) (srcHeight * widthRatio));
		} else {
			scale((int) (srcWidth * heightRatio), height);
		}
		
		// 获取缩放后的新的宽和高
		srcImage = getValidSrcImg();
		srcHeight = srcImage.getHeight(null);
		srcWidth = srcImage.getWidth(null);

		if (null == fixedColor) {// 补白
			fixedColor = Color.WHITE;
		}
		final BufferedImage image = new BufferedImage(width, height, getTypeInt());
		Graphics2D g = image.createGraphics();

		// 设置背景
		g.setBackground(fixedColor);
		g.clearRect(0, 0, width, height);

		// 在中间贴图
		g.drawImage(srcImage, (width - srcWidth) / 2, (height - srcHeight) / 2, srcWidth, srcHeight, fixedColor, null);

		g.dispose();
		this.targetImage = image;
		return this;
	}

	/**
	 * 图像切割(按指定起点坐标和宽高切割)
	 * 
	 * @param rectangle 矩形对象，表示矩形区域的x，y，width，height
	 * @return this
	 */
	public Img cut(Rectangle rectangle) {
		final Image srcImage = getValidSrcImg();
		rectangle = fixRectangle(rectangle, srcImage.getWidth(null), srcImage.getHeight(null));

		final ImageFilter cropFilter = new CropImageFilter(rectangle.x, rectangle.y, rectangle.width, rectangle.height);
		final Image image = Toolkit.getDefaultToolkit().createImage(new FilteredImageSource(srcImage.getSource(), cropFilter));
		this.targetImage = ImgUtil.toBufferedImage(image);
		return this;
	}

	/**
	 * 图像切割为圆形(按指定起点坐标和半径切割)，填充满整个图片（直径取长宽最小值）
	 * 
	 * @param x 原图的x坐标起始位置
	 * @param y 原图的y坐标起始位置
	 * @return this
	 * @since 4.1.15
	 */
	public Img cut(int x, int y) {
		return cut(x, y, -1);
	}

	/**
	 * 图像切割为圆形(按指定起点坐标和半径切割)
	 * 
	 * @param x 原图的x坐标起始位置
	 * @param y 原图的y坐标起始位置
	 * @param radius 半径，小于0表示填充满整个图片（直径取长宽最小值）
	 * @return this
	 * @since 4.1.15
	 */
	public Img cut(int x, int y, int radius) {
		final Image srcImage = getValidSrcImg();
		final int width = srcImage.getWidth(null);
		final int height = srcImage.getHeight(null);

		// 计算直径
		final int diameter = radius > 0 ? radius * 2 : Math.min(width, height);
		final BufferedImage targetImage = new BufferedImage(diameter, diameter, BufferedImage.TYPE_INT_ARGB);
		final Graphics2D g = targetImage.createGraphics();
		g.setClip(new Ellipse2D.Double(0, 0, diameter, diameter));

		if (this.positionBaseCentre) {
			x = x - width / 2 + diameter / 2;
			y = y - height / 2 + diameter / 2;
		}
		g.drawImage(srcImage, x, y, null);
		g.dispose();
		this.targetImage = targetImage;
		return this;
	}

	/**
	 * 图片圆角处理
	 * 
	 * @param arc 圆角弧度，0~1，为长宽占比
	 * @return this
	 * @since 4.5.3
	 */
	public Img round(double arc) {
		final Image srcImage = getValidSrcImg();
		final int width = srcImage.getWidth(null);
		final int height = srcImage.getHeight(null);

		// 通过弧度占比计算弧度
		arc = NumberUtil.mul(arc, Math.min(width, height));

		final BufferedImage targetImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
		final Graphics2D g2 = targetImage.createGraphics();
		g2.setComposite(AlphaComposite.Src);
		// 抗锯齿
		g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		g2.fill(new RoundRectangle2D.Double(0, 0, width, height, arc, arc));
		g2.setComposite(AlphaComposite.SrcAtop);
		g2.drawImage(srcImage, 0, 0, null);
		g2.dispose();
		this.targetImage = targetImage;
		return this;
	}

	/**
	 * 彩色转为黑白
	 * 
	 * @return this
	 */
	public Img gray() {
		final ColorConvertOp op = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_GRAY), null);
		this.targetImage = op.filter(ImgUtil.toBufferedImage(getValidSrcImg()), null);
		return this;
	}

	/**
	 * 彩色转为黑白二值化图片
	 * 
	 * @return this
	 */
	public Img binary() {
		this.targetImage = ImgUtil.copyImage(getValidSrcImg(), BufferedImage.TYPE_BYTE_BINARY);
		return this;
	}

	/**
	 * 给图片添加文字水印<br>
	 * 此方法并不关闭流
	 * 
	 * @param pressText 水印文字
	 * @param color 水印的字体颜色
	 * @param font {@link Font} 字体相关信息
	 * @param x 修正值。 默认在中间，偏移量相对于中间偏移
	 * @param y 修正值。 默认在中间，偏移量相对于中间偏移
	 * @param alpha 透明度：alpha 必须是范围 [0.0, 1.0] 之内（包含边界值）的一个浮点数字
	 * @return 处理后的图像
	 */
	public Img pressText(String pressText, Color color, Font font, int x, int y, float alpha) {
		final BufferedImage targetImage = ImgUtil.toBufferedImage(getValidSrcImg());
		final Graphics2D g = targetImage.createGraphics();

		if (null == font) {
			// 默认字体
			font = new Font("Courier", Font.PLAIN, (int) (targetImage.getHeight() * 0.75));
		}

		// 抗锯齿
		g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		g.setColor(color);
		g.setFont(font);
		// 透明度
		g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha));
		// 在指定坐标绘制水印文字
		final FontMetrics metrics = g.getFontMetrics(font);
		final int textLength = metrics.stringWidth(pressText);
		final int textHeight = metrics.getAscent() - metrics.getLeading() - metrics.getDescent();
		g.drawString(pressText, Math.abs(targetImage.getWidth() - textLength) / 2 + x, Math.abs(targetImage.getHeight() + textHeight) / 2 + y);
		g.dispose();
		this.targetImage = targetImage;

		return this;
	}

	/**
	 * 给图片添加图片水印
	 * 
	 * @param pressImg 水印图片，可以使用{@link ImageIO#read(File)}方法读取文件
	 * @param x 修正值。 默认在中间，偏移量相对于中间偏移
	 * @param y 修正值。 默认在中间，偏移量相对于中间偏移
	 * @param alpha 透明度：alpha 必须是范围 [0.0, 1.0] 之内（包含边界值）的一个浮点数字
	 * @return this
	 */
	public Img pressImage(Image pressImg, int x, int y, float alpha) {
		final int pressImgWidth = pressImg.getWidth(null);
		final int pressImgHeight = pressImg.getHeight(null);

		return pressImage(pressImg, new Rectangle(x, y, pressImgWidth, pressImgHeight), alpha);
	}

	/**
	 * 给图片添加图片水印
	 * 
	 * @param pressImg 水印图片，可以使用{@link ImageIO#read(File)}方法读取文件
	 * @param rectangle 矩形对象，表示矩形区域的x，y，width，height，x,y从背景图片中心计算
	 * @param alpha 透明度：alpha 必须是范围 [0.0, 1.0] 之内（包含边界值）的一个浮点数字
	 * @return this
	 * @since 4.1.14
	 */
	public Img pressImage(Image pressImg, Rectangle rectangle, float alpha) {
		final Image targetImg = getValidSrcImg();

		rectangle = fixRectangle(rectangle, targetImg.getWidth(null), targetImg.getHeight(null));
		this.targetImage = draw(ImgUtil.toBufferedImage(targetImg), pressImg, rectangle, alpha);
		return this;
	}

	/**
	 * 旋转图片为指定角度<br>
	 * 来自：http://blog.51cto.com/cping1982/130066
	 * 
	 * @param degree 旋转角度
	 * @return 旋转后的图片
	 * @since 3.2.2
	 */
	public Img rotate(int degree) {
		final Image image = getValidSrcImg();
		int width = image.getWidth(null);
		int height = image.getHeight(null);
		final Rectangle rectangle = calcRotatedSize(width, height, degree);
		final BufferedImage targetImg = new BufferedImage(rectangle.width, rectangle.height, getTypeInt());
		Graphics2D graphics2d = targetImg.createGraphics();
		// 抗锯齿
		graphics2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
		// 从中心旋转
		graphics2d.translate((rectangle.width - width) / 2, (rectangle.height - height) / 2);
		graphics2d.rotate(Math.toRadians(degree), width / 2, height / 2);
		graphics2d.drawImage(image, 0, 0, null);
		graphics2d.dispose();
		this.targetImage = targetImg;
		return this;
	}

	/**
	 * 水平翻转图像
	 * 
	 * @return this
	 */
	public Img flip() {
		final Image image = getValidSrcImg();
		int width = image.getWidth(null);
		int height = image.getHeight(null);
		final BufferedImage targetImg = new BufferedImage(width, height, getTypeInt());
		Graphics2D graphics2d = targetImg.createGraphics();
		graphics2d.drawImage(image, 0, 0, width, height, width, 0, 0, height, null);
		graphics2d.dispose();
		this.targetImage = targetImg;
		return this;
	}

	// ----------------------------------------------------------------------------------------------------------------- Write
	/**
	 * 获取处理过的图片
	 * 
	 * @return 处理过的图片
	 */
	public Image getImg() {
		return this.targetImage;
	}

	/**
	 * 写出图像
	 * 
	 * @param out 写出到的目标流
	 * @return 是否成功写出，如果返回false表示未找到合适的Writer
	 * @throws IORuntimeException IO异常
	 */
	public boolean write(OutputStream out) throws IORuntimeException {
		return write(ImgUtil.getImageOutputStream(out));
	}

	/**
	 * 写出图像为PNG格式
	 * 
	 * @param targetImageStream 写出到的目标流
	 * @return 是否成功写出，如果返回false表示未找到合适的Writer
	 * @throws IORuntimeException IO异常
	 */
	public boolean write(ImageOutputStream targetImageStream) throws IORuntimeException {
		Assert.notBlank(this.targetImageType, "Target image type is blank !");
		Assert.notNull(targetImageStream, "Target output stream is null !");

		final Image targetImage = (null == this.targetImage) ? this.srcImage : this.targetImage;
		Assert.notNull(targetImage, "Target image is null !");

		return ImgUtil.write(targetImage, this.targetImageType, targetImageStream, this.quality);
	}

	/**
	 * 写出图像为目标文件扩展名对应的格式
	 * 
	 * @param targetFile 目标文件
	 * @return 是否成功写出，如果返回false表示未找到合适的Writer
	 * @throws IORuntimeException IO异常
	 */
	public boolean write(File targetFile) throws IORuntimeException {
		final String formatName = FileUtil.extName(targetFile);
		if (StrUtil.isNotBlank(formatName)) {
			this.targetImageType = formatName;
		}

		if (targetFile.exists()) {
			targetFile.delete();
		}

		ImageOutputStream out = null;
		try {
			out = ImgUtil.getImageOutputStream(targetFile);
			return write(out);
		} finally {
			IoUtil.close(out);
		}
	}

	// ---------------------------------------------------------------------------------------------------------------- Private method start
	/**
	 * 将图片绘制在背景上
	 * 
	 * @param backgroundImg 背景图片
	 * @param img 要绘制的图片
	 * @param rectangle 矩形对象，表示矩形区域的x，y，width，height，x,y从背景图片中心计算
	 * @return 绘制后的背景
	 */
	private static BufferedImage draw(BufferedImage backgroundImg, Image img, Rectangle rectangle, float alpha) {
		final Graphics2D g = backgroundImg.createGraphics();
		g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha));
		g.drawImage(img, rectangle.x, rectangle.y, rectangle.width, rectangle.height, null); // 绘制切割后的图
		g.dispose();
		return backgroundImg;
	}

	/**
	 * 获取int类型的图片类型
	 * 
	 * @return 图片类型
	 * @see BufferedImage#TYPE_INT_ARGB
	 * @see BufferedImage#TYPE_INT_RGB
	 */
	private int getTypeInt() {
		switch (this.targetImageType) {
		case ImgUtil.IMAGE_TYPE_PNG:
			return BufferedImage.TYPE_INT_ARGB;
		default:
			return BufferedImage.TYPE_INT_RGB;
		}
	}

	/**
	 * 获取有效的源图片，首先检查上一次处理的结果图片，如无则使用用户传入的源图片
	 * 
	 * @return 有效的源图片
	 */
	private Image getValidSrcImg() {
		return ObjectUtil.defaultIfNull(this.targetImage, this.srcImage);
	}

	/**
	 * 修正矩形框位置，如果{@link Img#setPositionFromCentre(boolean)} 设为{@code true}，则坐标修正为基于图形中心，否则基于左上角
	 * 
	 * @param rectangle 矩形
	 * @param baseWidth 参考宽
	 * @param baseHeight 参考高
	 * @return 修正后的{@link Rectangle}
	 * @since 4.1.15
	 */
	private Rectangle fixRectangle(Rectangle rectangle, int baseWidth, int baseHeight) {
		if (this.positionBaseCentre) {
			// 修正图片位置从背景的中心计算
			rectangle.setLocation(//
					rectangle.x + (int) (Math.abs(baseWidth - rectangle.width) / 2), //
					rectangle.y + (int) (Math.abs(baseHeight - rectangle.height) / 2)//
			);
		}
		return rectangle;
	}

	/**
	 * 计算旋转后的图片尺寸
	 * 
	 * @param width 宽度
	 * @param height 高度
	 * @param degree 旋转角度
	 * @return 计算后目标尺寸
	 * @since 4.1.20
	 */
	private static Rectangle calcRotatedSize(int width, int height, int degree) {
		if (degree >= 90) {
			if (degree / 90 % 2 == 1) {
				int temp = height;
				height = width;
				width = temp;
			}
			degree = degree % 90;
		}
		double r = Math.sqrt(height * height + width * width) / 2;
		double len = 2 * Math.sin(Math.toRadians(degree) / 2) * r;
		double angel_alpha = (Math.PI - Math.toRadians(degree)) / 2;
		double angel_dalta_width = Math.atan((double) height / width);
		double angel_dalta_height = Math.atan((double) width / height);
		int len_dalta_width = (int) (len * Math.cos(Math.PI - angel_alpha - angel_dalta_width));
		int len_dalta_height = (int) (len * Math.cos(Math.PI - angel_alpha - angel_dalta_height));
		int des_width = width + len_dalta_width * 2;
		int des_height = height + len_dalta_height * 2;

		return new Rectangle(des_width, des_height);
	}
	// ---------------------------------------------------------------------------------------------------------------- Private method end
}
