/*
 * 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.util;

import icy.math.MathUtil;

import java.awt.Color;
import java.awt.color.ColorSpace;

/**
 * Color utilities class.
 * 
 * @author Stephane
 */
public class ColorUtil
{
    /**
     * RGB colorSpace
     */
    public final static ColorSpace sRGB = ColorSpace.getInstance(ColorSpace.CS_sRGB);

    /**
     * Basic rainbow colors
     */
    private final static Color[] colors = generateRainbow(32, true, true, true);

    /**
     * Returns a random color.
     */
    public static Color getRandomColor()
    {
        return colors[Random.nextInt(20)];
    }

    /**
     * Generates a rainbow color table (HSV ramp) of the specified size.
     * 
     * @param saturation
     *        saturation factor (from 0 to 1).
     * @param brightness
     *        brightness factor (from 0 to 1).
     * @param size
     *        the size of rainbow color table.
     * @param black
     *        if true the table will also contains a black color entry.
     * @param white
     *        if true the table will also contains a white color entry.
     * @param gray
     *        if true the table will also contains a gray color entry.
     */
    public static Color[] generateRainbow(float saturation, float brightness, int size, boolean black, boolean white,
            boolean gray)
    {
        final Color[] result = new Color[size];

        int start = 0;
        if (black)
            result[start++] = Color.black;
        if (white)
            result[start++] = Color.white;
        if (gray)
            result[start++] = Color.gray;

        for (int i = start; i < result.length; i++)
            result[i] = Color.getHSBColor((float) (i - start) / (float) (size - start), saturation, brightness);

        return result;
    }

    /**
     * Generates a rainbow color table (HSV ramp) of the specified size.
     * 
     * @param size
     *        the size of the rainbow color table.
     * @param black
     *        if true the table will also contains a black color entry.
     * @param white
     *        if true the table will also contains a white color entry.
     * @param gray
     *        if true the table will also contains a gray color entry.
     */
    public static Color[] generateRainbow(int size, boolean black, boolean white, boolean gray)
    {
        return generateRainbow(1f, 1f, size, black, white, gray);
    }

    /**
     * Generates a rainbow color table (HSV ramp) of the specified size.
     * 
     * @param size
     *        the size of the HSV color table.
     */
    public static Color[] generateRainbow(int size)
    {
        return generateRainbow(size, false, false, false);
    }

    /**
     * Get String representation of the specified color.<br>
     * <br>
     * Default representation is "A:R:G:B" where :<br>
     * A = alpha level in hexadecimal (0x00-0xFF)<br>
     * R = red level in hexadecimal (0x00-0xFF)<br>
     * G = green level in hexadecimal (0x00-0xFF)<br>
     * B = blue level in hexadecimal (0x00-0xFF)<br>
     * 
     * @param color
     */
    public static String toString(Color color)
    {
        return toString(color, true, ":");
    }

    /**
     * Get String representation of the specified rgb value.<br>
     * <br>
     * Default representation is "A:R:G:B" where :<br>
     * A = alpha level in hexadecimal (00-FF)<br>
     * R = red level in hexadecimal (00-FF)<br>
     * G = green level in hexadecimal (00-FF)<br>
     * B = blue level in hexadecimal (00-FF)<br>
     * 
     * @param rgb
     */
    public static String toString(int rgb)
    {
        return toString(rgb, true, ":");
    }

    /**
     * Get String representation of the specified Color value.<br>
     * <br>
     * Default representation is "A:R:G:B" where :<br>
     * A = alpha level<br>
     * R = red level<br>
     * G = green level<br>
     * B = blue level<br>
     * 
     * @param color
     * @param hexa
     *        component level are represented in hexadecimal (2 digits)
     */
    public static String toString(Color color, boolean hexa)
    {
        return toString(color, hexa, ":");
    }

    /**
     * Get String representation of the specified rgb value.<br>
     * <br>
     * Default representation is "A:R:G:B" where :<br>
     * A = alpha level<br>
     * R = red level<br>
     * G = green level<br>
     * B = blue level<br>
     * 
     * @param rgb
     * @param hexa
     *        component level are represented in hexadecimal (2 digits)
     */
    public static String toString(int rgb, boolean hexa)
    {
        return toString(rgb, hexa, ":");
    }

    /**
     * Get String representation of the specified color.<br>
     * <br>
     * Default representation is "AsepRsepGsepB" where :<br>
     * A = alpha level in hexadecimal (0x00-0xFF)<br>
     * R = red level in hexadecimal (0x00-0xFF)<br>
     * G = green level in hexadecimal (0x00-0xFF)<br>
     * B = blue level in hexadecimal (0x00-0xFF)<br>
     * sep = the specified separator<br>
     * <br>
     * Ex : toString(Color.red, true, ":") --> "FF:FF:00:00"
     * 
     * @param color
     * @param hexa
     *        component level are represented in hexadecimal (2 digits)
     */
    public static String toString(Color color, boolean hexa, String sep)
    {
        if (color == null)
            return "-";

        return toString(color.getRGB(), hexa, sep);
    }

    /**
     * Get String representation of the specified rgb value.<br>
     * <br>
     * Default representation is "AsepRsepGsepB" where :<br>
     * A = alpha level in hexadecimal (0x00-0xFF)<br>
     * R = red level in hexadecimal (0x00-0xFF)<br>
     * G = green level in hexadecimal (0x00-0xFF)<br>
     * B = blue level in hexadecimal (0x00-0xFF)<br>
     * sep = the specified separator<br>
     * <br>
     * Ex : toString(0xFF00FF00, true, ":") --> "FF:00:FF:00"
     * 
     * @param rgb
     * @param hexa
     *        component level are represented in hexadecimal (2 digits)
     */
    public static String toString(int rgb, boolean hexa, String sep)
    {
        final int a = (rgb >> 24) & 0xFF;
        final int r = (rgb >> 16) & 0xFF;
        final int g = (rgb >> 8) & 0xFF;
        final int b = (rgb >> 0) & 0xFF;

        if (hexa)
            return (StringUtil.toHexaString(a, 2) + sep + StringUtil.toHexaString(r, 2) + sep
                    + StringUtil.toHexaString(g, 2) + sep + StringUtil.toHexaString(b, 2)).toUpperCase();

        return StringUtil.toString(a) + sep + StringUtil.toString(r) + sep + StringUtil.toString(g) + sep
                + StringUtil.toString(b);
    }

    /**
     * Returns <code>true</code> if the specified color is pure black (alpha is not verified)
     */
    public static boolean isBlack(Color color)
    {
        return (color.getRGB() & 0x00FFFFFF) == 0;
    }

    /**
     * Mix 2 colors with priority color
     */
    public static Color mixOver(Color backColor, Color frontColor)
    {
        final int r, g, b, a;

        final float frontAlpha = frontColor.getAlpha() / 255f;
        final float invAlpha = 1f - frontAlpha;

        r = (int) ((backColor.getRed() * invAlpha) + (frontColor.getRed() * frontAlpha));
        g = (int) ((backColor.getGreen() * invAlpha) + (frontColor.getGreen() * frontAlpha));
        b = (int) ((backColor.getBlue() * invAlpha) + (frontColor.getBlue() * frontAlpha));
        a = Math.max(backColor.getAlpha(), frontColor.getAlpha());

        return new Color(r, g, b, a);
    }

    /**
     * Mix 2 colors using the following ratio for mixing:<br/>
     * 0f means 100% of color 1 and 0% of color 2<br/>
     * 0.5f means 50% of color 1 and 50% of color 2<br/>
     * 1f means 0% of color 1 and 100% of color 2
     */
    public static Color mix(Color c1, Color c2, float ratio)
    {
        final int r, g, b;
        final float r2 = Math.min(1f, Math.max(0f, ratio));
        final float r1 = 1f - r2;

        r = (int) ((c1.getRed() * r1) + (c2.getRed() * r2));
        g = (int) ((c1.getGreen() * r1) + (c2.getGreen() * r2));
        b = (int) ((c1.getBlue() * r1) + (c2.getBlue() * r2));

        return new Color(r, g, b);
    }

    /**
     * Mix 2 colors without "priority" color
     */
    public static Color mix(Color c1, Color c2, boolean useAlpha)
    {
        final int r, g, b, a;

        if (useAlpha)
        {
            final float a1 = c1.getAlpha() / 255f;
            final float a2 = c2.getAlpha() / 255f;
            final float af = a1 + a2;

            r = (int) (((c1.getRed() * a1) + (c2.getRed() * a2)) / af);
            g = (int) (((c1.getGreen() * a1) + (c2.getGreen() * a2)) / af);
            b = (int) (((c1.getBlue() * a1) + (c2.getBlue() * a2)) / af);
            a = Math.max(c1.getAlpha(), c2.getAlpha());
        }
        else
        {
            r = (c1.getRed() + c2.getRed()) / 2;
            g = (c1.getGreen() + c2.getGreen()) / 2;
            b = (c1.getBlue() + c2.getBlue()) / 2;
            a = 255;
        }

        return new Color(r, g, b, a);
    }

    /**
     * Mix 2 colors (no alpha)
     */
    public static Color mix(Color c1, Color c2)
    {
        return mix(c1, c2, false);
    }

    /**
     * Add 2 colors
     */
    public static Color add(Color c1, Color c2, boolean useAlpha)
    {
        final int r, g, b, a;

        r = Math.min(c1.getRed() + c2.getRed(), 255);
        g = Math.min(c1.getGreen() + c2.getGreen(), 255);
        b = Math.min(c1.getBlue() + c2.getBlue(), 255);

        if (useAlpha)
            a = Math.max(c1.getAlpha(), c2.getAlpha());
        else
            a = 255;

        return new Color(r, g, b, a);
    }

    /**
     * Add 2 colors
     */
    public static Color add(Color c1, Color c2)
    {
        return add(c1, c2, false);
    }

    /**
     * Sub 2 colors
     */
    public static Color sub(Color c1, Color c2, boolean useAlpha)
    {
        final int r, g, b, a;

        r = Math.max(c1.getRed() - c2.getRed(), 0);
        g = Math.max(c1.getGreen() - c2.getGreen(), 0);
        b = Math.max(c1.getBlue() - c2.getBlue(), 0);

        if (useAlpha)
            a = Math.max(c1.getAlpha(), c2.getAlpha());
        else
            a = 255;

        return new Color(r, g, b, a);
    }

    /**
     * Subtract 2 colors
     */
    public static Color sub(Color c1, Color c2)
    {
        return sub(c1, c2, false);
    }

    /**
     * Get opposite (XORed) color
     */
    public static Color xor(Color c)
    {
        return new Color(c.getRed() ^ 0xFF, c.getGreen() ^ 0xFF, c.getBlue() ^ 0xFF, c.getAlpha());
    }

    /**
     * get to gray level (simple RGB mix)
     */
    public static int getGrayMix(Color c)
    {
        return getGrayMix(c.getRGB());
    }

    /**
     * get to gray level (simple RGB mix)
     */
    public static int getGrayMix(int rgb)
    {
        return (((rgb >> 16) & 0xFF) + ((rgb >> 8) & 0xFF) + ((rgb >> 0) & 0xFF)) / 3;
    }

    /**
     * Convert to gray level color (simple RGB mix)
     */
    public static Color getGrayColorMix(Color c)
    {
        final int gray = getGrayMix(c);
        return new Color(gray, gray, gray);
    }

    /**
     * Convert to gray level color (from luminance calculation)
     */
    public static Color getGrayColorLum(Color c)
    {
        final int gray = getLuminance(c);
        return new Color(gray, gray, gray);
    }

    /**
     * Return luminance (in [0..255] range)
     */
    public static int getLuminance(Color c)
    {
        return (int) ((c.getRed() * 0.299) + (c.getGreen() * 0.587) + (c.getBlue() * 0.114));
    }

    /**
     * Convert the specified color to HSV color.
     */
    public static float[] toHSV(Color c)
    {
        return toHSV(c.getRGBColorComponents(null));
    }

    /**
     * Convert the specified RGB color to HSV color.
     */
    public static float[] toHSV(float[] rgb)
    {
        float r = rgb[0];
        float g = rgb[1];
        float b = rgb[2];
        float min, max, delta;
        float h, s, v;

        min = Math.min(r, Math.min(g, b));
        max = Math.max(r, Math.max(g, b));

        // black
        if (max == 0f)
            return new float[] {0, 0, 0};

        v = max;
        delta = max - min;
        s = delta / max;

        // graylevel
        if (delta == 0f)
            return new float[] {0, s, v};

        if (r == max)
            // between yellow & magenta
            h = (g - b) / delta;
        else if (g == max)
            // between cyan & yellow
            h = 2 + (b - r) / delta;
        else
            // between magenta & cyan
            h = 4 + (r - g) / delta;

        // want positif hue
        if (h < 0)
            h += 6f;

        return new float[] {h / 6f, s, v};
    }

    /**
     * Convert the specified HSV color to RGB color.
     */
    public static float[] fromHSV(float[] hsv)
    {
        float h = hsv[0];
        float s = hsv[0];
        float v = hsv[0];
        float f, p, q, t;
        float r, g, b;
        int i;

        // no color
        if (s == 0f)
            return new float[] {v, v, v};

        // sector 0 to 5
        h *= 6f;
        i = (int) Math.floor(h);
        // factorial part of h
        f = h - i;
        p = v * (1f - s);
        q = v * (1f - (s * f));
        t = v * (1f - (s * (1 - f)));

        switch (i)
        {
            case 0:
                r = v;
                g = t;
                b = p;
                break;
            case 1:
                r = q;
                g = v;
                b = p;
                break;
            case 2:
                r = p;
                g = v;
                b = t;
                break;
            case 3:
                r = p;
                g = q;
                b = v;
                break;
            case 4:
                r = t;
                g = p;
                b = v;
                break;
            default:
                r = v;
                g = p;
                b = q;
                break;
        }

        return new float[] {r, g, b};
    }

    /**
     * Convert the specified XYZ color to RGB color.
     */
    public static float[] fromXYZ(float[] xyz)
    {
        return sRGB.fromCIEXYZ(xyz);
    }

    /**
     * Convert the specified color to XYZ color.
     */
    public static float[] toXYZ(Color c)
    {
        return toXYZ(c.getRGBColorComponents(null));
    }

    /**
     * Convert the specified RGB color to XYZ color.
     */
    public static float[] toXYZ(float[] rgb)
    {
        return sRGB.toCIEXYZ(rgb);
    }

    /**
     * Convert the specified color to LAB color.
     */
    public static float[] toLAB(Color c)
    {
        return toLAB(c.getRGBColorComponents(null));
    }

    /**
     * Convert the specified RGB color to LAB color.
     */
    public static float[] toLAB(float[] rgb)
    {
        return XYZtoLAB(toXYZ(rgb));
    }

    private static float pivotXYZ(float value)
    {
        return (value > 0.008856f) ? (float) MathUtil.cubicRoot(value) : (7.787f * value) + 0.1379f;
    }

    /**
     * Convert the specified XYZ color to LAB color.
     */
    public static float[] XYZtoLAB(float[] xyz)
    {
        float x = pivotXYZ(xyz[0] / 95.047f);
        float y = pivotXYZ(xyz[1] / 100f);
        float z = pivotXYZ(xyz[2] / 108.883f);

        float l = Math.max(0, (116f * y) - 16f);
        float a = 500f * (x - y);
        float b = 200f * (y - z);

        return new float[] {l, a, b};
    }

    /**
     * Compute and returns the distance between the 2 colors.<br>
     * The HSV distance returns a value between 0 and 1 where 1 is maximum distance.<br>
     * The LAB distance returns a positive value where > 2.3 value is considered a
     * significant distance.
     * 
     * @param c1
     *        first color
     * @param c2
     *        second color
     * @param hsv
     *        If set to true we use the HSV color space to compute the color distance otherwise we
     *        use the LAB color space.
     */
    public static double getDistance(Color c1, Color c2, boolean hsv)
    {
        if (hsv)
        {
            // use HSV color space
            final float[] hsv1 = toHSV(c1);
            final float[] hsv2 = toHSV(c2);

            return getDistance(hsv1, hsv2, true);
        }

        // use LAB color space
        final float[] lab1 = toLAB(c1);
        final float[] lab2 = toLAB(c2);

        return getDistance(lab1, lab2, true);
    }

    /**
     * Returns the distance between 2 colors from same color space.
     */
    static double getDistance(float[] c1, float[] c2, boolean compareThirdComponent)
    {
        float result = (float) (Math.pow(c1[0] - c2[0], 2d) + Math.pow(c1[1] - c2[1], 2d));

        if (compareThirdComponent)
            result += Math.pow(c1[2] - c2[2], 2d);

        return result;
    }

    /**
     * Returns the dominant color from the specified color array.<br>
     * The dominant color is calculated by computing the color histogram from a rainbow gradient and
     * returning the highest bin number.
     */
    public static Color getDominantColor(Color colors[])
    {
        return getDominantColor(colors, 33);
    }

    /**
     * Returns the dominant color from the specified color array.<br>
     * The dominant color is calculated by computing the color histogram from a rainbow gradient and
     * returning the color corresponding to the highest bin.
     * 
     * @param colors
     *        Color array we want to retrieve the dominant color from.
     * @param binNumber
     *        the number of bin to construct the rainbow gradient.
     */
    public static Color getDominantColor(Color colors[], int binNumber)
    {
        final Color[] baseColors = generateRainbow(1f, 1f, binNumber, false, false, true);

        final float[][] colorsHSV = new float[colors.length][];
        final float[][] baseColorsHSV = new float[binNumber][];

        // convert colors to HSV float component
        for (int i = 0; i < colors.length; i++)
            colorsHSV[i] = toHSV(colors[i]);
        for (int i = 0; i < baseColors.length; i++)
            baseColorsHSV[i] = toHSV(baseColors[i]);

        final int[] bins = new int[binNumber];

        for (float[] colorHsv : colorsHSV)
        {
            double minDist = getDistance(colorHsv, baseColorsHSV[0], true);
            int minInd = 0;

            for (int ind = 1; ind < baseColorsHSV.length; ind++)
            {
                final double dist = getDistance(colorHsv, baseColorsHSV[ind], true);

                if (dist < minDist)
                {
                    minDist = dist;
                    minInd = ind;
                }
            }

            bins[minInd]++;
        }

        int max = bins[0];
        int maxInd = 0;

        for (int i = 1; i < bins.length; i++)
        {
            final int v = bins[i];

            if (v > max)
            {
                max = v;
                maxInd = i;
            }
        }

        return baseColors[maxInd];
    }

    /**
     * Converts a wavelength into a {@link Color} object.<br/>
     * Taken from Earl F. Glynn's web page:
     * <a href="http://www.efg2.com/Lab/ScienceAndEngineering/Spectra.htm">Spectra Lab Report</a>
     * 
     * @param wavelength
     *        the wavelength to convert (in nanometers)
     * @return a {@link Color} object representing the specified wavelength
     */
    public static Color getColorFromWavelength(double wavelength)
    {
        double factor;
        double r, g, b;

        if ((wavelength >= 380) && (wavelength < 440))
        {
            r = -(wavelength - 440) / (440 - 380);
            g = 0.0;
            b = 1.0;
        }
        else if ((wavelength >= 440) && (wavelength < 490))
        {
            r = 0.0;
            g = (wavelength - 440) / (490 - 440);
            b = 1.0;
        }
        else if ((wavelength >= 490) && (wavelength < 510))
        {
            r = 0.0;
            g = 1.0;
            b = -(wavelength - 510) / (510 - 490);
        }
        else if ((wavelength >= 510) && (wavelength < 580))
        {
            r = (wavelength - 510) / (580 - 510);
            g = 1.0;
            b = 0.0;
        }
        else if ((wavelength >= 580) && (wavelength < 645))
        {
            r = 1.0;
            g = -(wavelength - 645) / (645 - 580);
            b = 0.0;
        }
        else if ((wavelength >= 645) && (wavelength < 781))
        {
            r = 1.0;
            g = 0.0;
            b = 0.0;
        }
        else
        {
            r = 0.0;
            g = 0.0;
            b = 0.0;
        }

        // Let the intensity fall off near the vision limits
        if ((wavelength >= 380) && (wavelength < 420))
            factor = 0.3 + 0.7 * (wavelength - 380) / (420 - 380);
        else if ((wavelength >= 420) && (wavelength < 701))
            factor = 1.0;
        else if ((wavelength >= 701) && (wavelength < 781))
            factor = 0.3 + 0.7 * (780 - wavelength) / (780 - 700);
        else
            factor = 0.0;

        int[] rgb = new int[3];

        rgb[0] = r == 0.0 ? 0 : (int) Math.round(255 * r * factor);
        rgb[1] = g == 0.0 ? 0 : (int) Math.round(255 * g * factor);
        rgb[2] = b == 0.0 ? 0 : (int) Math.round(255 * b * factor);

        return new Color(rgb[0], rgb[1], rgb[2]);
    }
}