package com.zcloud.basic.info.domain.utils;

import com.alibaba.cola.exception.BizException;
import org.springframework.web.multipart.MultipartFile;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.Iterator;
import java.util.Locale;

public class ImageCompressUtil {

    private static final String JPG_SUFFIX = ".jpg";
    private static final String JPG_CONTENT_TYPE = "image/jpeg";
    private static final long DEFAULT_MAX_SIZE_KB = 100L;
    private static final float MIN_QUALITY = 0.05F;
    private static final float MAX_QUALITY = 1.0F;
    private static final int QUALITY_SEARCH_TIMES = 8;
    private static final double RESIZE_SCALE = 0.9D;

    private ImageCompressUtil() {
    }

    public static MultipartFile compressToJpg(MultipartFile file) throws IOException {
        return compressToJpg(file, DEFAULT_MAX_SIZE_KB);
    }

    public static MultipartFile compressToJpg(MultipartFile file, long maxSizeKb) {
        try {
            if (file == null || file.isEmpty()) {
                throw new IllegalArgumentException("file can not be null or empty");
            }
            if (maxSizeKb <= 0) {
                throw new IllegalArgumentException("maxSizeKb must be greater than 0");
            }

            long maxSizeBytes = maxSizeKb * 1024L;
            byte[] sourceBytes = file.getBytes();
            String formatName = getFormatName(sourceBytes);
            if (isTargetJpg(file, formatName, maxSizeBytes)) {
                return file;
            }

            BufferedImage bufferedImage = ImageIO.read(new ByteArrayInputStream(sourceBytes));
            if (bufferedImage == null) {
                throw new IllegalArgumentException("file is not a supported image");
            }

            BufferedImage rgbImage = toRgbImage(bufferedImage);
            byte[] compressedBytes = compress(rgbImage, maxSizeBytes);
            String targetFileName = toJpgFileName(file.getOriginalFilename(), file.getName());
            return new ByteArrayMultipartFile(file.getName(), targetFileName, JPG_CONTENT_TYPE, compressedBytes);
        } catch (Exception e){
            e.printStackTrace();
            throw new BizException(e.getMessage());
        }

    }

    private static boolean isTargetJpg(MultipartFile file, String formatName, long maxSizeBytes) {
        return file.getSize() <= maxSizeBytes
                && isJpgFormat(formatName)
                && hasJpgSuffix(file.getOriginalFilename());
    }

    private static boolean isJpgFormat(String formatName) {
        if (formatName == null) {
            return false;
        }
        String lowerFormatName = formatName.toLowerCase(Locale.ROOT);
        return "jpg".equals(lowerFormatName) || "jpeg".equals(lowerFormatName);
    }

    private static boolean hasJpgSuffix(String fileName) {
        if (fileName == null) {
            return false;
        }
        return fileName.toLowerCase(Locale.ROOT).endsWith(JPG_SUFFIX);
    }

    private static String getFormatName(byte[] sourceBytes) throws IOException {
        try (ImageInputStream imageInputStream = ImageIO.createImageInputStream(new ByteArrayInputStream(sourceBytes))) {
            Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg");
            if (!writers.hasNext()) {
                throw new IOException("jpg writer not found");
            }
            Iterator<javax.imageio.ImageReader> readers = ImageIO.getImageReaders(imageInputStream);
            if (!readers.hasNext()) {
                return null;
            }
            javax.imageio.ImageReader imageReader = readers.next();
            try {
                return imageReader.getFormatName();
            } finally {
                imageReader.dispose();
            }
        }
    }

    private static BufferedImage toRgbImage(BufferedImage sourceImage) {
        if (sourceImage.getType() == BufferedImage.TYPE_INT_RGB) {
            return sourceImage;
        }
        BufferedImage rgbImage = new BufferedImage(sourceImage.getWidth(), sourceImage.getHeight(), BufferedImage.TYPE_INT_RGB);
        Graphics2D graphics = rgbImage.createGraphics();
        try {
            graphics.setColor(Color.WHITE);
            graphics.fillRect(0, 0, sourceImage.getWidth(), sourceImage.getHeight());
            graphics.drawImage(sourceImage, 0, 0, null);
        } finally {
            graphics.dispose();
        }
        return rgbImage;
    }

    private static byte[] compress(BufferedImage image, long maxSizeBytes) throws IOException {
        BufferedImage currentImage = image;
        while (true) {
            byte[] compressedBytes = searchBestQuality(currentImage, maxSizeBytes);
            if (compressedBytes != null) {
                return compressedBytes;
            }
            if (currentImage.getWidth() == 1 && currentImage.getHeight() == 1) {
                throw new IOException("can not compress image to target size");
            }
            currentImage = resize(currentImage, RESIZE_SCALE);
        }
    }

    private static byte[] searchBestQuality(BufferedImage image, long maxSizeBytes) throws IOException {
        byte[] maxQualityBytes = writeJpg(image, MAX_QUALITY);
        if (maxQualityBytes.length <= maxSizeBytes) {
            return maxQualityBytes;
        }

        byte[] minQualityBytes = writeJpg(image, MIN_QUALITY);
        if (minQualityBytes.length > maxSizeBytes) {
            return null;
        }

        byte[] bestBytes = minQualityBytes;
        float low = MIN_QUALITY;
        float high = MAX_QUALITY;
        for (int i = 0; i < QUALITY_SEARCH_TIMES; i++) {
            float mid = (low + high) / 2;
            byte[] currentBytes = writeJpg(image, mid);
            if (currentBytes.length <= maxSizeBytes) {
                bestBytes = currentBytes;
                low = mid;
            } else {
                high = mid;
            }
        }
        return bestBytes;
    }

    private static byte[] writeJpg(BufferedImage image, float quality) throws IOException {
        Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg");
        if (!writers.hasNext()) {
            throw new IOException("jpg writer not found");
        }

        ImageWriter imageWriter = writers.next();
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        try (ImageOutputStream imageOutputStream = ImageIO.createImageOutputStream(outputStream)) {
            imageWriter.setOutput(imageOutputStream);
            ImageWriteParam writeParam = imageWriter.getDefaultWriteParam();
            if (writeParam.canWriteCompressed()) {
                writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
                writeParam.setCompressionQuality(quality);
            }
            imageWriter.write(null, new IIOImage(image, null, null), writeParam);
        } finally {
            imageWriter.dispose();
        }
        return outputStream.toByteArray();
    }

    private static BufferedImage resize(BufferedImage sourceImage, double scale) {
        int targetWidth = Math.max(1, (int) Math.round(sourceImage.getWidth() * scale));
        int targetHeight = Math.max(1, (int) Math.round(sourceImage.getHeight() * scale));
        BufferedImage resizedImage = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB);
        Graphics2D graphics = resizedImage.createGraphics();
        try {
            graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
            graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
            graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            graphics.drawImage(sourceImage, 0, 0, targetWidth, targetHeight, null);
        } finally {
            graphics.dispose();
        }
        return resizedImage;
    }

    private static String toJpgFileName(String originalFileName, String fallbackName) {
        String fileName = originalFileName;
        if (fileName == null || fileName.trim().isEmpty()) {
            fileName = fallbackName;
        }
        if (fileName == null || fileName.trim().isEmpty()) {
            fileName = "image";
        }
        int dotIndex = fileName.lastIndexOf('.');
        if (dotIndex > -1) {
            fileName = fileName.substring(0, dotIndex);
        }
        return fileName + JPG_SUFFIX;
    }

    private static class ByteArrayMultipartFile implements MultipartFile {

        private final String name;
        private final String originalFilename;
        private final String contentType;
        private final byte[] content;

        private ByteArrayMultipartFile(String name, String originalFilename, String contentType, byte[] content) {
            this.name = name;
            this.originalFilename = originalFilename;
            this.contentType = contentType;
            this.content = content;
        }

        @Override
        public String getName() {
            return name;
        }

        @Override
        public String getOriginalFilename() {
            return originalFilename;
        }

        @Override
        public String getContentType() {
            return contentType;
        }

        @Override
        public boolean isEmpty() {
            return content.length == 0;
        }

        @Override
        public long getSize() {
            return content.length;
        }

        @Override
        public byte[] getBytes() {
            return content;
        }

        @Override
        public InputStream getInputStream() {
            return new ByteArrayInputStream(content);
        }

        @Override
        public void transferTo(File dest) throws IOException, IllegalStateException {
            Files.write(dest.toPath(), content);
        }
    }
}
