/*
 * Copyright 2010-2015 Institut Pasteur.
 * 
 * This file is part of Icy.
 * 
 * Icy is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Icy 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 General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
 */
package icy.image;

import icy.gui.util.FontUtil;
import icy.network.URLUtil;
import icy.system.thread.ThreadUtil;
import icy.util.GraphicsUtil;

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.Transparency;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.awt.image.IndexColorModel;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.WritableRaster;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;

import javax.imageio.ImageIO;

/**
 * Image utilities class.
 * 
 * @author stephane
 */
public class ImageUtil
{
    public static String getImageTypeString(int type)
    {
        switch (type)
        {
            case BufferedImage.TYPE_CUSTOM:
                return "TYPE_CUSTOM";
            case BufferedImage.TYPE_INT_RGB:
                return "TYPE_INT_RGB";
            case BufferedImage.TYPE_INT_ARGB:
                return "TYPE_INT_ARGB";
            case BufferedImage.TYPE_INT_ARGB_PRE:
                return "TYPE_INT_ARGB_PRE";
            case BufferedImage.TYPE_INT_BGR:
                return "TYPE_INT_BGR";
            case BufferedImage.TYPE_3BYTE_BGR:
                return "TYPE_3BYTE_BGR";
            case BufferedImage.TYPE_4BYTE_ABGR:
                return "TYPE_4BYTE_ABGR";
            case BufferedImage.TYPE_4BYTE_ABGR_PRE:
                return "TYPE_4BYTE_ABGR_PRE";
            case BufferedImage.TYPE_USHORT_565_RGB:
                return "TYPE_USHORT_565_RGB";
            case BufferedImage.TYPE_USHORT_555_RGB:
                return "TYPE_USHORT_555_RGB";
            case BufferedImage.TYPE_BYTE_GRAY:
                return "TYPE_BYTE_GRAY";
            case BufferedImage.TYPE_USHORT_GRAY:
                return "TYPE_USHORT_GRAY";
            case BufferedImage.TYPE_BYTE_BINARY:
                return "TYPE_BYTE_BINARY";
            case BufferedImage.TYPE_BYTE_INDEXED:
                return "TYPE_BYTE_INDEXED";
            default:
                return "UNKNOWN TYPE";
        }
    }

    public static String getTransparencyString(int transparency)
    {
        switch (transparency)
        {
            case Transparency.OPAQUE:
                return "OPAQUE";
            case Transparency.BITMASK:
                return "BITMASK";
            case Transparency.TRANSLUCENT:
                return "TRANSLUCENT";
            default:
                return "UNKNOWN TRANSPARENCY";
        }
    }

    /**
     * Wait for dimension information of specified image being loaded.
     * 
     * @param image
     *        image we are waiting informations for.
     */
    public static void waitImageReady(Image image)
    {
        if (image != null)
        {
            final long st = System.currentTimeMillis();

            // wait 2 seconds max
            while ((image.getWidth(null) == -1) && ((System.currentTimeMillis() - st) < 2000))
                ThreadUtil.sleep(1);
        }
    }

    /**
     * Create a 8 bits indexed buffered image from specified <code>IndexColorModel</code><br>
     * and byte array data.
     */
    public static BufferedImage createIndexedImage(int w, int h, IndexColorModel cm, byte[] data)
    {
        final WritableRaster raster = Raster.createInterleavedRaster(new DataBufferByte(data, w * h, 0), w, h, w, 1,
                new int[] {0}, null);

        return new BufferedImage(cm, raster, false, null);
    }

    /**
     * Load an image from specified path
     */
    public static BufferedImage load(String path, boolean displayError)
    {
        return load(URLUtil.getURL(path), displayError);
    }

    /**
     * Load an image from specified path
     */
    public static BufferedImage load(String path)
    {
        return load(path, true);
    }

    /**
     * Load an image from specified url
     */
    public static BufferedImage load(URL url, boolean displayError)
    {
        if (url != null)
        {
            try
            {
                return ImageIO.read(url);
            }
            catch (IOException e)
            {
                if (displayError)
                    System.err.println("Can't load image from " + url);
            }
        }

        return null;
    }

    /**
     * Asynchronously load an image from specified url.<br/>
     * Use {@link #waitImageReady(Image)} to know if width and height property
     */
    public static Image loadAsync(URL url)
    {
        return Toolkit.getDefaultToolkit().createImage(url);
    }

    /**
     * Asynchronously load an image from specified path.<br/>
     * Use {@link #waitImageReady(Image)} to know if width and height property
     */
    public static Image loadAsync(String path)
    {
        return Toolkit.getDefaultToolkit().createImage(path);
    }

    /**
     * Load an image from specified url
     */
    public static BufferedImage load(URL url)
    {
        return load(url, true);
    }

    /**
     * Load an image from specified file
     */
    public static BufferedImage load(File file, boolean displayError)
    {
        if (file != null)
        {
            try
            {
                return ImageIO.read(file);
            }
            catch (IOException e)
            {
                if (displayError)
                    System.err.println("Can't load image from " + file);
            }
        }

        return null;
    }

    /**
     * Load an image from specified file
     */
    public static BufferedImage load(File file)
    {
        return loadImage(file, true);
    }

    /**
     * Load an image from specified InputStream
     */
    public static BufferedImage load(InputStream input, boolean displayError)
    {
        if (input != null)
        {
            try
            {
                return ImageIO.read(input);
            }
            catch (Exception e)
            {
                if (displayError)
                    System.err.println("Can't load image from stream " + input);
            }
        }

        return null;
    }

    /**
     * Load an image from specified InputStream
     */
    public static BufferedImage load(InputStream input)
    {
        return load(input, true);
    }

    /**
     * @deprecated Use {@link ImageUtil#load(String, boolean)} instead
     */
    @Deprecated
    public static BufferedImage loadImage(String path, boolean displayError)
    {
        return load(path, displayError);
    }

    /**
     * @deprecated Use {@link ImageUtil#load(String)} instead
     */
    @Deprecated
    public static BufferedImage loadImage(String path)
    {
        return load(path);
    }

    /**
     * @deprecated Use {@link ImageUtil#load(URL, boolean)} instead
     */
    @Deprecated
    public static BufferedImage loadImage(URL url, boolean displayError)
    {
        return load(url, displayError);
    }

    /**
     * @deprecated Use {@link ImageUtil#load(URL)} instead
     */
    @Deprecated
    public static Image loadImage(URL url)
    {
        return load(url);
    }

    /**
     * @deprecated Use {@link ImageUtil#load(File, boolean)} instead
     */
    @Deprecated
    public static BufferedImage loadImage(File file, boolean displayError)
    {
        return load(file, displayError);
    }

    /**
     * @deprecated Use {@link ImageUtil#load(File)} instead
     */
    @Deprecated
    public static BufferedImage loadImage(File file)
    {
        return load(file);
    }

    /**
     * @deprecated Use {@link ImageUtil#load(InputStream, boolean)} instead
     */
    @Deprecated
    public static BufferedImage loadImage(InputStream input, boolean displayError)
    {
        return load(input, displayError);

    }

    /**
     * @deprecated Use {@link ImageUtil#load(InputStream)} instead
     */
    @Deprecated
    public static BufferedImage loadImage(InputStream input)
    {
        return load(input);

    }

    /**
     * Save an image to specified path in specified format
     */
    public static boolean save(RenderedImage image, String format, String path)
    {
        if (path != null)
        {
            try
            {
                return ImageIO.write(image, format, new FileOutputStream(path));
            }
            catch (IOException e)
            {
                System.err.println("Can't save image to " + path);
            }
        }

        return false;
    }

    /**
     * Save an image to specified file in specified format
     */
    public static boolean save(RenderedImage image, String format, File file)
    {
        if (file != null)
        {
            try
            {
                return ImageIO.write(image, format, file);
            }
            catch (IOException e)
            {
                System.err.println("Can't save image to " + file);
            }
        }

        return false;
    }

    /**
     * @deprecated Use {@link ImageUtil#save(RenderedImage, String, String)} instead
     */
    @Deprecated
    public static boolean saveImage(RenderedImage image, String format, String path)
    {
        return save(image, format, path);
    }

    /**
     * @deprecated Use {@link ImageUtil#save(RenderedImage, String, File)} instead
     */
    @Deprecated
    public static boolean saveImage(RenderedImage image, String format, File file)
    {
        return save(image, format, file);
    }

    /**
     * Return a RenderedImage from the given Image object.
     */
    public static RenderedImage toRenderedImage(Image image)
    {
        return toBufferedImage(image);
    }

    /**
     * Return a ARGB BufferedImage from the given Image object.
     * If the image is already a BufferedImage image then it's directly returned
     */
    public static BufferedImage toBufferedImage(Image image)
    {
        if (image instanceof BufferedImage)
            return (BufferedImage) image;

        // be sure image data are ready
        waitImageReady(image);
        final BufferedImage bufImage = new BufferedImage(image.getWidth(null), image.getHeight(null),
                BufferedImage.TYPE_INT_ARGB);

        final Graphics2D g = bufImage.createGraphics();
        g.drawImage(image, 0, 0, null);
        g.dispose();

        return bufImage;
    }

    /**
     * Scale an image with specified size.
     */
    public static BufferedImage scale(Image image, int width, int height)
    {
        if (image != null)
        {
            final BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
            final Graphics2D g = result.createGraphics();

            g.setComposite(AlphaComposite.Src);
            g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
            g.drawImage(image, 0, 0, width, height, null);
            g.dispose();

            return result;
        }

        return null;
    }

    /**
     * Scale an image with specified size (try to keep best quality).
     */
    public static BufferedImage scaleQuality(Image image, int width, int height)
    {
        if (image != null)
        {
            Image current = image;

            // be sure image data are ready
            waitImageReady(image);

            int w = image.getWidth(null);
            int h = image.getHeight(null);

            do
            {
                if (w > width)
                {
                    w /= 2;
                    if (w < width)
                        w = width;
                }
                else
                    w = width;

                if (h > height)
                {
                    h /= 2;
                    if (h < height)
                        h = height;
                }
                else
                    h = height;

                final BufferedImage result = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
                final Graphics2D g = result.createGraphics();

                g.setComposite(AlphaComposite.Src);
                g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
                g.drawImage(current, 0, 0, w, h, null);

                g.dispose();

                current = result;
            }
            while (w != width || h != height);

            return (BufferedImage) current;
        }

        return null;
    }

    /**
     * Convert an image to a BufferedImage.<br>
     * If <code>out</out> is null, by default a <code>BufferedImage.TYPE_INT_ARGB</code> is created.
     */
    public static BufferedImage convert(Image in, BufferedImage out)
    {
        final BufferedImage result;

        // be sure image data are ready
        waitImageReady(in);

        // no output type specified ? use ARGB
        if (out == null)
            result = new BufferedImage(in.getWidth(null), in.getHeight(null), BufferedImage.TYPE_INT_ARGB);
        else
            result = out;

        final Graphics g = result.getGraphics();
        g.drawImage(in, 0, 0, null);
        g.dispose();

        return result;
    }

    /**
     * Returns <code>true</code> if the specified image is a grayscale image whatever is the image
     * type (GRAY, RGB, ARGB...)
     */
    public static boolean isGray(BufferedImage image)
    {
        if (image == null)
            return false;

        if (image.getType() == BufferedImage.TYPE_BYTE_GRAY)
            return true;
        if (image.getType() == BufferedImage.TYPE_USHORT_GRAY)
            return true;

        final int[] rgbArray = image.getRGB(0, 0, image.getWidth(), image.getHeight(), null, 0, image.getWidth());

        for (int value : rgbArray)
        {
            final int c0 = (value >> 0) & 0xFF;
            final int c1 = (value >> 8) & 0xFF;
            if (c0 != c1)
                return false;

            final int c2 = (value >> 16) & 0xFF;
            if (c0 != c2)
                return false;
        }

        return true;
    }

    /**
     * Convert an image to grey image (<code>BufferedImage.TYPE_BYTE_GRAY</code>).
     */
    public static BufferedImage toGray(Image image)
    {
        if (image != null)
        {
            // be sure image data are ready
            waitImageReady(image);
            return convert(image, new BufferedImage(image.getWidth(null), image.getHeight(null),
                    BufferedImage.TYPE_BYTE_GRAY));
        }

        return null;
    }

    /**
     * Convert an image to RGB image (<code>BufferedImage.TYPE_INT_RGB</code>).
     */
    public static BufferedImage toRGBImage(Image image)
    {
        if (image != null)
        {
            // be sure image data are ready
            waitImageReady(image);
            return convert(image, new BufferedImage(image.getWidth(null), image.getHeight(null),
                    BufferedImage.TYPE_INT_RGB));
        }

        return null;
    }

    /**
     * Convert an image to ARGB image (<code>BufferedImage.TYPE_INT_ARGB</code>).
     */
    public static BufferedImage toARGBImage(Image image)
    {
        if (image != null)
        {
            // be sure image data are ready
            waitImageReady(image);
            return convert(image, new BufferedImage(image.getWidth(null), image.getHeight(null),
                    BufferedImage.TYPE_INT_ARGB));
        }

        return null;
    }

    /**
     * @deprecated Use {@link ImageUtil#scale(Image, int, int)} instead.
     */
    @Deprecated
    public static BufferedImage scaleImage(Image image, int width, int height)
    {
        return scale(image, width, height);
    }

    /**
     * @deprecated Use {@link ImageUtil#scaleQuality(Image, int, int)} instead.
     */
    @Deprecated
    public static BufferedImage scaleImageQuality(Image image, int width, int height)
    {
        return scaleQuality(image, width, height);
    }

    /**
     * @deprecated Use {@link ImageUtil#convert(Image, BufferedImage)} instead.
     */
    @Deprecated
    public static BufferedImage convertImage(Image in, BufferedImage out)
    {
        return convert(in, out);
    }

    /**
     * @deprecated Use {@link ImageUtil#toGray(Image)} instead.
     */
    @Deprecated
    public static BufferedImage toGrayImage(Image image)
    {
        return toGray(image);
    }

    /**
     * Create a copy of the input image.<br>
     * Result is always a <code>BufferedImage.TYPE_INT_ARGB</code> type image.
     */
    public static BufferedImage getCopy(Image in)
    {
        return convert(in, null);
    }

    /**
     * Return true if image has the same size
     */
    public static boolean sameSize(BufferedImage im1, BufferedImage im2)
    {
        return (im1.getWidth() == im2.getWidth()) && (im1.getHeight() == im2.getHeight());
    }

    /**
     * Apply simple color filter with specified alpha factor to the image
     */
    public static void applyColorFilter(Image image, Color color, float alpha)
    {
        if (image != null)
        {
            // be sure image data are ready
            waitImageReady(image);

            // should be Graphics2D compatible
            final Graphics2D g = (Graphics2D) image.getGraphics();
            final Rectangle rect = new Rectangle(image.getWidth(null), image.getHeight(null));

            g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
            g.setColor(color);
            g.fill(rect);
            g.dispose();
        }
    }

    /**
     * Return an image which contains specified color depending original alpha intensity image
     */
    public static Image getColorImageFromAlphaImage(Image alphaImage, Color color)
    {
        return paintColorImageFromAlphaImage(alphaImage, null, color);
    }

    /**
     * Paint the specified color in 'out' image depending original alpha intensity from 'alphaImage'
     */
    public static Image paintColorImageFromAlphaImage(Image alphaImage, Image out, Color color)
    {
        final int w;
        final int h;
        final Image result;

        if (out == null)
        {
            // be sure image data are ready
            waitImageReady(alphaImage);

            w = alphaImage.getWidth(null);
            h = alphaImage.getHeight(null);

            if ((w == -1) || (h == -1))
                return null;

            result = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
        }
        else
        {
            // be sure image data are ready
            waitImageReady(out);

            w = out.getWidth(null);
            h = out.getHeight(null);

            if ((w == -1) || (h == -1))
                return null;

            result = out;
        }

        final Graphics2D g = (Graphics2D) result.getGraphics();

        // clear
        g.setBackground(new Color(0x00000000, true));
        g.clearRect(0, 0, w, h);

        // be sure image data are ready
        waitImageReady(alphaImage);
        // draw icon
        g.drawImage(alphaImage, 0, 0, null);

        // set fill color
        g.setComposite(AlphaComposite.SrcAtop);
        g.setColor(color);
        g.fillRect(0, 0, w, h);

        g.dispose();

        return result;
    }

    /**
     * Draw text in the specified image with specified parameters.<br>
     */
    public static void drawText(Image image, String text, float x, float y, int size, Color color)
    {
        final Graphics2D g = (Graphics2D) image.getGraphics();

        // prepare setting
        g.setColor(color);
        g.setFont(FontUtil.setSize(g.getFont(), size));
        g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

        // draw icon
        g.drawString(text, x, y);

        g.dispose();
    }

    /**
     * Draw text at top right in the specified image with specified parameters.<br>
     */
    public static void drawTextTopRight(Image image, String text, int size, boolean bold, Color color)
    {
        final Graphics2D g = (Graphics2D) image.getGraphics();

        // prepare setting
        g.setColor(color);
        g.setFont(FontUtil.setSize(g.getFont(), size));
        if (bold)
            g.setFont(FontUtil.setStyle(g.getFont(), Font.BOLD));
        g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

        // get string bounds
        final Rectangle2D bounds = GraphicsUtil.getStringBounds(g, text);

        // be sure image data are ready
        waitImageReady(image);

        final float w = image.getWidth(null);

        // draw text
        g.drawString(text, w - ((float) bounds.getWidth()), 0 - (float) bounds.getY());

        g.dispose();
    }
}