/*
 * $Id: ImageUtils.java 9131 2014-07-15 09:35:13Z uckelman $
 *
 * Copyright (c) 2007-2010 by Joel Uckelman
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License (LGPL) as published by the Free Software Foundation.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, copies are available
 * at http://www.opensource.org.
 */

package VASSAL.tools.image;

import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.PixelGrabber;
import java.awt.image.WritableRaster;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

import javax.swing.ImageIcon;

import VASSAL.Info;
import VASSAL.tools.ErrorDialog;
import VASSAL.tools.io.TemporaryFileFactory;

public class ImageUtils {
  private ImageUtils() {}

  // FIXME: We should fix this, eventually.
  // negative, because historically we've done it this way
  private static final double DEGTORAD = -Math.PI/180.0;

  private static final GeneralFilter.Filter upscale =
    new GeneralFilter.MitchellFilter();
  private static final GeneralFilter.Filter downscale =
    new GeneralFilter.Lanczos3Filter();

  @Deprecated
  public static final String SCALER_ALGORITHM = "scalerAlgorithm"; //$NON-NLS-1$
  private static final Map<RenderingHints.Key,Object> defaultHints =
    new HashMap<RenderingHints.Key,Object>();

  static {
    // Initialise Image prefs prior to Preferences being read.

    // set up map for creating default RenderingHints
    defaultHints.put(RenderingHints.KEY_INTERPOLATION,
                     RenderingHints.VALUE_INTERPOLATION_BILINEAR);
    defaultHints.put(RenderingHints.KEY_ANTIALIASING,
                     RenderingHints.VALUE_ANTIALIAS_ON);
  }

  /** @deprecated All scaling is done with the high-quality scaler now. */
  @Deprecated
  public static void setHighQualityScaling(boolean b) {}

  public static RenderingHints getDefaultHints() {
    return new RenderingHints(defaultHints);
  }

  public static Rectangle transform(Rectangle srect,
                                    double scale,
                                    double angle) {
    final AffineTransform t = AffineTransform.getRotateInstance(
      DEGTORAD*angle, srect.getCenterX(), srect.getCenterY());
    t.scale(scale, scale);
    return t.createTransformedShape(srect).getBounds();
  }

  public static BufferedImage transform(BufferedImage src,
                                        double scale,
                                        double angle) {
    return transform(src, scale, angle, getDefaultHints());
  }

  public static BufferedImage transform(BufferedImage src,
                                        double scale,
                                        double angle,
                                        RenderingHints hints) {
    // bail on null source
    if (src == null) return null;

    // nothing to do, return source
    if (scale == 1.0 && angle == 0.0) {
      return src;
    }

    // return null image if scaling makes source vanish
    if (src.getWidth() * scale == 0 || src.getHeight() * scale == 0) {
      return NULL_IMAGE;
    }

    // use the default hints if we weren't given any
    if (hints == null) hints = getDefaultHints();

    if (scale == 1.0 && angle % 90.0 == 0.0) {
      // this is an unscaled quadrant rotation, we can do this simply
      hints.put(RenderingHints.KEY_INTERPOLATION,
                RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
      hints.put(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_OFF);

      final Rectangle ubox = getBounds(src);
      final Rectangle tbox = transform(ubox, scale, angle);

      // keep opaque destination for orthogonal rotation of an opaque source
      final BufferedImage trans = createCompatibleImage(
        tbox.width,
        tbox.height,
        src.getTransparency() != BufferedImage.OPAQUE
      );

      final AffineTransform t = new AffineTransform();
      t.translate(-tbox.x, -tbox.y);
      t.rotate(DEGTORAD*angle, ubox.getCenterX(), ubox.getCenterY());
      t.scale(scale, scale);
      t.translate(ubox.x, ubox.y);

      final Graphics2D g = trans.createGraphics();
      g.setRenderingHints(hints);
      g.drawImage(src, t, null);
      g.dispose();
      return trans;
    }
    else {
      if (angle != 0.0) {
        final Rectangle ubox = getBounds(src);
// FIXME: this duplicates the standard scaling case
// FIXME: check whether AffineTransformOp is faster

        final Rectangle rbox = transform(ubox, 1.0, angle);

        // keep opaque destination for orthogonal rotation of an opaque source
        final BufferedImage rot = createCompatibleImage(
          rbox.width,
          rbox.height,
          src.getTransparency() != BufferedImage.OPAQUE || angle % 90.0 != 0.0
        );

// FIXME: rotation via bilinear interpolation probably decreases quality
        final AffineTransform tx = new AffineTransform();
        tx.translate(-rbox.x, -rbox.y);
        tx.rotate(DEGTORAD*angle, ubox.getCenterX(), ubox.getCenterY());
        tx.translate(ubox.x, ubox.y);

        final Graphics2D g = rot.createGraphics();
        g.setRenderingHints(hints);
        g.drawImage(src, tx, null);
        g.dispose();
        src = rot;
      }

      if (scale != 1.0) {
        src = coerceToIntType(src);

        final Rectangle sbox = transform(getBounds(src), scale, 0.0);

        // return null image if scaling makes source vanish
        if (sbox.width == 0 || sbox.height == 0) {
          return NULL_IMAGE;
        }

        final BufferedImage dst =
          GeneralFilter.zoom(sbox, src, scale > 1.0 ? upscale : downscale);

        return toCompatibleImage(dst);
      }
      else {
        return src;
      }
    }
  }

  @Deprecated
  public static BufferedImage transform(BufferedImage src,
                                        double scale,
                                        double angle,
                                        RenderingHints hints,
                                        int quality) {
    return transform(src, scale, angle, hints);
  }

  @SuppressWarnings("fallthrough")
  public static BufferedImage coerceToIntType(BufferedImage img) {
    // ensure that img is a type which GeneralFilter can handle
    switch (img.getType()) {
    case BufferedImage.TYPE_INT_RGB:
    case BufferedImage.TYPE_INT_ARGB:
    case BufferedImage.TYPE_INT_ARGB_PRE:
    case BufferedImage.TYPE_INT_BGR:
      return img;
    default:
      return toType(img, img.getTransparency() == BufferedImage.OPAQUE ?
        BufferedImage.TYPE_INT_RGB :
        getCompatibleTranslucentImageType() == BufferedImage.TYPE_INT_ARGB ?
          BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_ARGB_PRE);
    }
  }

  /**
   * @param im
   * @return the boundaries of this image, where (0,0) is the
   * pseudo-center of the image
   */
  public static Rectangle getBounds(BufferedImage im) {
    return new Rectangle(-im.getWidth()/2,
                         -im.getHeight()/2,
                          im.getWidth(),
                          im.getHeight());
  }

  public static Rectangle getBounds(Dimension d) {
    return new Rectangle(-d.width / 2,
                         -d.height / 2,
                          d.width,
                          d.height);
  }

  /** @deprecated Use {@link #getImageSize(String,InputStream)} instead. */
  @Deprecated
  public static Dimension getImageSize(InputStream in) throws IOException {
    return getImageSize("", in);
  }

  private static final TemporaryFileFactory tfac = new TemporaryFileFactory() {
    public File create() throws IOException {
      return File.createTempFile("img", null, Info.getTempDir());
    }
  };

  private static final ImageLoader loader =
    new ImageIOImageLoader(new FallbackImageTypeConverter(tfac));

  public static Dimension getImageSize(String name, InputStream in)
                                                      throws ImageIOException {
    return loader.size(name, in);
  }

  /** @deprecated Use {@link #getImage(String,InputStream)} instead. */
  @Deprecated
  public static BufferedImage getImage(InputStream in) throws IOException {
    return getImage("", in);
  }

  public static BufferedImage getImageResource(String name)
                                                      throws ImageIOException {
    final InputStream in = ImageUtils.class.getResourceAsStream(name);
    if (in == null) throw new ImageNotFoundException(name);
    return getImage(name, in);
  }

  public static BufferedImage getImage(String name, InputStream in)
                                                      throws ImageIOException {
    return loader.load(
      name, in, compatOpaqueImageType, compatTranslImageType, true
    );
  }

  public static BufferedImage toType(BufferedImage src, int type) {
    final BufferedImage dst =
      new BufferedImage(src.getWidth(), src.getHeight(), type);

    final Graphics2D g = dst.createGraphics();
    g.drawImage(src, 0, 0, null);
    g.dispose();

    return dst;
  }

  public static Image forceLoad(Image img) {
    // ensure that the image is loaded
    return new ImageIcon(img).getImage();
  }

  public static boolean isTransparent(Image img) {
    // determine whether this image has an alpha channel
    final PixelGrabber pg = new PixelGrabber(img, 0, 0, 1, 1, false);
    try {
      pg.grabPixels();
    }
    catch (InterruptedException e) {
      ErrorDialog.bug(e);
    }

    return pg.getColorModel().hasAlpha();
  }

  public static boolean isTransparent(BufferedImage img) {
    return img.getTransparency() != BufferedImage.OPAQUE;
  }

  /**
   * Transform an <code>Image</code> to a <code>BufferedImage</code>.
   *
   * @param src the <code>Image</code> to transform
   */
  public static BufferedImage toBufferedImage(Image src) {
    if (src == null) return null;
    if (src instanceof BufferedImage)
      return toCompatibleImage((BufferedImage) src);

    // ensure that the image is loaded
    src = forceLoad(src);

    final BufferedImage dst = createCompatibleImage(
      src.getWidth(null), src.getHeight(null), isTransparent(src)
    );

    final Graphics2D g = dst.createGraphics();
    g.drawImage(src, 0, 0, null);
    g.dispose();

    return dst;
  }

  protected static final boolean IS_MAC_RETINA;

  static {
    final Object o = Toolkit.getDefaultToolkit().getDesktopProperty(
      "apple.awt.contentScaleFactor"
    );

    IS_MAC_RETINA = (o instanceof Number) && ((Number) o).doubleValue() == 2.0;
  }

  public static boolean isMacRetina() {
    return IS_MAC_RETINA;
  }

  private static boolean isHeadless() {
    return GraphicsEnvironment.isHeadless();
  }

  private static GraphicsConfiguration getGraphicsConfiguration() {
    return GraphicsEnvironment.getLocalGraphicsEnvironment().
             getDefaultScreenDevice().getDefaultConfiguration();
  }

  protected static final BufferedImage compatOpaqueImage;
  protected static final BufferedImage compatTransImage;

  protected static final int compatOpaqueImageType;
  protected static final int compatTranslImageType;

  static {
    BufferedImage oimg;
    BufferedImage timg;

    if (isHeadless()) {
      oimg = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
      timg = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
    }
    else {
      final GraphicsConfiguration gc = getGraphicsConfiguration();

      oimg = gc.createCompatibleImage(1,1, BufferedImage.OPAQUE);
      timg = gc.createCompatibleImage(1,1, BufferedImage.TRANSLUCENT);

      // Bug workaround: MacOX X machines with Retina displays are incapable
      // of painting TYPE_INT_ARGB_PRE images, despite that these systems
      // return that type as the "compatible" image type.
      if (isMacRetina()) {
        if (oimg.getType() == BufferedImage.TYPE_INT_ARGB_PRE) {
          oimg = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
        }

        if (timg.getType() == BufferedImage.TYPE_INT_ARGB_PRE) {
          timg = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
        }
      }
    }

    compatOpaqueImage = oimg;
    compatTransImage = timg;

    compatOpaqueImageType = compatOpaqueImage.getType();
    compatTranslImageType = compatTransImage.getType();
  }

  public static final BufferedImage NULL_IMAGE = createCompatibleImage(1,1);

  public static int getCompatibleImageType() {
    return compatOpaqueImageType;
  }

  public static int getCompatibleTranslucentImageType() {
    return compatTranslImageType;
  }

  public static int getCompatibleImageType(boolean transparent) {
    return transparent ? compatTranslImageType : compatOpaqueImageType;
  }

  public static int getCompatibleImageType(BufferedImage img) {
    return getCompatibleImageType(isTransparent(img));
  }

  public static BufferedImage createCompatibleImage(int w, int h) {
    final ColorModel cm = compatOpaqueImage.getColorModel();
    final WritableRaster wr = cm.createCompatibleWritableRaster(w, h);
    return new BufferedImage(cm, wr, cm.isAlphaPremultiplied(), null);
  }

  public static BufferedImage createCompatibleImage(int w, int h,
                                                    boolean transparent) {
    return transparent ?
      createCompatibleTranslucentImage(w, h) :
      createCompatibleImage(w, h);
  }

  public static BufferedImage createCompatibleTranslucentImage(int w, int h) {
    final ColorModel cm = compatTransImage.getColorModel();
    final WritableRaster wr = cm.createCompatibleWritableRaster(w, h);
    return new BufferedImage(cm, wr, cm.isAlphaPremultiplied(), null);
  }

  public static BufferedImage toCompatibleImage(BufferedImage src) {
    if ((src.getColorModel().equals(compatOpaqueImage.getColorModel()) &&
         src.getTransparency() == compatOpaqueImage.getTransparency())
        ||
        (src.getColorModel().equals(compatTransImage.getColorModel()) &&
         src.getTransparency() == compatTransImage.getTransparency()))
    {
      return src;
    }

    final BufferedImage dst = createCompatibleImage(
      src.getWidth(), src.getHeight(), isTransparent(src)
    );

    final Graphics2D g = dst.createGraphics();
    g.drawImage(src, 0, 0, null);
    g.dispose();

    return dst;
  }

  public static boolean isCompatibleImage(BufferedImage img) {
    return img.getType() ==
      getCompatibleImageType(img.getTransparency() != BufferedImage.OPAQUE);
  }

  /*
   * What Image suffixes does Vassal know about?
   * Used by the MassPieceLoader to identify candidate images.
   */
  public static final String GIF_SUFFIX = ".gif";
  public static final String PNG_SUFFIX = ".png";
  public static final String SVG_SUFFIX = ".svg";
  public static final String JPG_SUFFIX = ".jpg";
  public static final String JPEG_SUFFIX = ".jpeg";
  public static final String[] IMAGE_SUFFIXES = {
    GIF_SUFFIX, PNG_SUFFIX, SVG_SUFFIX, JPG_SUFFIX, JPEG_SUFFIX
  };

  public static boolean hasImageSuffix(String name) {
    final String s = name.toLowerCase();
    for (String suffix : IMAGE_SUFFIXES) {
      if (s.endsWith(suffix)) {
        return true;
      }
    }
    return false;
  }

  public static String stripImageSuffix(String name) {
    final String s = name.toLowerCase();
    for (String suffix : IMAGE_SUFFIXES) {
      if (s.endsWith(suffix)) {
        return name.substring(0, name.length()-suffix.length());
      }
    }
    return name;
  }
}