/**
 * This file is part of the Meeds project (https://meeds.io/).
 *
 * Copyright (C) 2020 - 2025 Meeds Association contact@meeds.io
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
package org.exoplatform.services.thumbnail;

import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Iterator;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.plugins.jpeg.JPEGImageWriteParam;
import javax.imageio.stream.ImageInputStream;

import org.imgscalr.Scalr;

import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

public class ImageResizeServiceImpl implements ImageResizeService {

  private static final Log LOG = ExoLogger.getLogger(ImageResizeServiceImpl.class);

  @Override
  public byte[] scaleImage(byte[] image, // NOSONAR
                           int width,
                           int height,
                           boolean fitExact,
                           boolean ultraQuality) throws Exception {
    if (width == 0 && height == 0) {
      return image;
    }
    Scalr.Method resizeMethod = ultraQuality ? Scalr.Method.ULTRA_QUALITY : Scalr.Method.QUALITY;
    BufferedImage bufferedImage = toBufferedImage(image);
    if (bufferedImage == null) {
      LOG.warn("Image could not be loaded using ImageIO API, original image will be returned instead of {}x{}", width, height);
      return image;
    }
    int originWidth = bufferedImage.getWidth();
    int originHeight = bufferedImage.getHeight();

    if (!fitExact && (width > originWidth || height > originHeight)) {
      // we don't want to increase image size, so return original image
      return image;
    }

    if (width == 0) {
      BufferedImage originalBufferedImage = bufferedImage;
      try {
        bufferedImage = Scalr.resize(bufferedImage, resizeMethod, Scalr.Mode.FIT_TO_HEIGHT, width, height, Scalr.OP_ANTIALIAS);
      } finally {
        flush(originalBufferedImage);
      }
    } else if (height == 0) {
      BufferedImage originalBufferedImage = bufferedImage;
      try {
        bufferedImage = Scalr.resize(bufferedImage, resizeMethod, Scalr.Mode.FIT_TO_WIDTH, width, height, Scalr.OP_ANTIALIAS);
      } finally {
        flush(originalBufferedImage);
      }
    } else if (fitExact) {
      BufferedImage originalBufferedImage = bufferedImage;
      try {
        bufferedImage = Scalr.resize(bufferedImage, resizeMethod, Scalr.Mode.FIT_EXACT, width, height, Scalr.OP_ANTIALIAS);
      } finally {
        flush(originalBufferedImage);
      }
    } else {
      float ratio = (float) originHeight / (float) originWidth;
      int calculatedTargetHeight = Math.round(width * ratio);

      Scalr.Mode fitMode = width > height ? Scalr.Mode.FIT_TO_WIDTH : Scalr.Mode.FIT_TO_HEIGHT;
      if (calculatedTargetHeight < height) {
        fitMode = Scalr.Mode.FIT_TO_HEIGHT;
      }
      BufferedImage originalBufferedImage = bufferedImage;
      try {
        bufferedImage = Scalr.resize(bufferedImage, resizeMethod, fitMode, width, height, Scalr.OP_ANTIALIAS);
      } finally {
        flush(originalBufferedImage);
      }
    }

    try {
      ImageReader imageReader = getImageReader(image);
      byte[] response = toByteArray(bufferedImage, imageReader);
      if (response.length == 0) {
        response = toByteArray(bufferedImage);
        // Use PNG Image Readear instead of Gif
        imageReader = getImageReader(response);
        response = toByteArray(bufferedImage, imageReader);
      }
      if (!fitExact && (response.length == 0 || response.length > image.length)) {
        // if the original image is smaller in weight from the resized image, we
        // must keep the original image
        return image;
      } else {
        return response;
      }
    } catch (IOException e) {
      LOG.error("Unable to read image to resize, return original size", e);
      return image;
    }

  }

  private ImageReader getImageReader(byte[] bytes) throws IOException {
    ImageInputStream imageInputStream = ImageIO.createImageInputStream(new ByteArrayInputStream(bytes));
    Iterator<ImageReader> readers = ImageIO.getImageReaders(imageInputStream);
    ImageReader reader = readers.next();
    reader.setInput(imageInputStream);
    return reader;
  }

  private BufferedImage toBufferedImage(byte[] imageBytes) {
    try {
      ByteArrayInputStream bis = new ByteArrayInputStream(imageBytes);
      return ImageIO.read(bis);
    } catch (Exception e) {
      LOG.error("Unable to read image", e);
      return null;
    }
  }

  private void flush(BufferedImage originalBufferedImage) {
    // call on the src to free up native resources and make it easier for
    // the GC to collect the unused image.
    originalBufferedImage.flush();
  }

  private byte[] toByteArray(BufferedImage targetBufferedImage, ImageReader sourceImageReader) throws IOException {
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    ImageWriter writer = ImageIO.getImageWriter(sourceImageReader);
    writer.setOutput(ImageIO.createImageOutputStream(byteArrayOutputStream));

    IIOMetadata metadata = sourceImageReader.getImageMetadata(0);
    ImageWriteParam param = writer.getDefaultWriteParam();
    if (param instanceof JPEGImageWriteParam jpegImageWriteParam) {
      jpegImageWriteParam.setOptimizeHuffmanTables(true);
    }
    writer.write(null, new IIOImage(targetBufferedImage, null, metadata), param);
    writer.dispose();
    return byteArrayOutputStream.toByteArray();
  }

  private byte[] toByteArray(BufferedImage bufferedImage) throws IOException {
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    ImageIO.write(bufferedImage, "png", byteArrayOutputStream);
    return byteArrayOutputStream.toByteArray();
  }

}
