/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2003-2006, Geotools Project Managment Committee (PMC)
 *    (C) 2001, Institut de Recherche pour le Développement
 *
 *    This library 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 2.1 of the License, or (at your option) any later version.
 *
 *    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
 *    Lesser General Public License for more details.
 */
package org.geotools.resources.image;

// J2SE dependencies
import java.awt.Color;
import java.awt.color.ColorSpace;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.IndexColorModel;
import java.math.BigInteger;
import java.util.Arrays;

// Geotools dependencies
import org.geotools.resources.XMath;


/**
 * A set of static methods for handling of colors informations. Some of those methods
 * are useful, but not really rigorous. This is why they do not appear in any "official"
 * package, but instead in this private one.
 *
 *                      <strong>Do not rely on this API!</strong>
 *
 * It may change in incompatible way in any future version.
 *
 * @since 2.0
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/coverage/src/main/java/org/geotools/resources/image/ColorUtilities.java $
 * @version $Id: ColorUtilities.java 26799 2007-08-31 21:31:21Z desruisseaux $
 * @author Martin Desruisseaux
 * @author Simone Giannecchini
 */
public final class ColorUtilities {
    /**
     * Small number for rounding errors.
     */
    private static final double EPS = 1E-6;

    /**
     * Do not allow creation of instances of this class.
     */
    private ColorUtilities() {
    }

    /**
     * Creates an sRGB color with the specified red, green, blue, and alpha
     * values in the range (0 - 255).
     * 
     * @param  r the red component
     * @param  g the green component
     * @param  b the blue component
     * @param  a the alpha component
     * @throws IllegalArgumentException if {@coder}, {@code g}, {@code b}
     *         or {@code a} are outside of the range 0 to 255, inclusive.
     */
    public static int getIntFromColor(int r, int g, int b, int a) {
        return ((a & 0xFF) << 24) |
               ((r & 0xFF) << 16) |
               ((g & 0xFF) <<  8) |
               ((b & 0xFF) <<  0);
    }

    /**
     * Returns a subarray of the specified color array. The {@code lower} and
     * {@code upper} index will be clamb into the {@code palette} range.
     * If they are completly out of range, or if they would result in an empty array,
     * then {@code null} is returned.
     *
     * This method is used by {@link org.geotools.cv.SampleDimension} as an heuristic
     * approach for distributing palette colors into a list of categories.
     *
     * @param  palette The color array (may be {@code null}).
     * @param  lower  The lower index, inclusive.
     * @param  upper  The upper index, inclusive.
     * @return The subarray (may be {@code palette} if the original array already fit),
     *         or {@code null} if the {@code lower} and {@code upper} index
     *         are out of {@code palette} bounds.
     */
    public static Color[] subarray(final Color[] palette, int lower, int upper) {
        if (palette != null) {
            lower = Math.max(lower, 0);
            upper = Math.min(upper, palette.length);
            if (lower >= upper) {
                return null;
            }
            if (lower!=0 || upper!=palette.length) {
                final Color[] sub = new Color[upper-lower];
                System.arraycopy(palette, lower, sub, 0, sub.length);
                return sub;
            }
        }
        return palette;
    }

    /**
     * Copy {@code colors} into array {@code ARGB} from index {@code lower}
     * inclusive to index {@code upper} exclusive. If {@code upper-lower} is not
     * equals to the length of {@code colors} array, then colors will be interpolated.
     *
     * @param colors Colors to copy into the {@code ARGB} array.
     * @param ARGB   Array of integer to write ARGB values to.
     * @param lower  Index (inclusive) of the first element of {@code ARGB} to change.
     * @param upper  Index (exclusive) of the last  element of {@code ARGB} to change.
     */
    public static void expand(final Color[] colors, final int[] ARGB,
                              final int lower, final int upper)
    {
        switch (colors.length) {
            case 1: Arrays.fill(ARGB, lower, upper, colors[0].getRGB()); // fall through
            case 0: return; // Note: getRGB() is really getARGB()
        }
        switch (upper-lower) {
            case 1: ARGB[lower] = colors[0].getRGB(); // fall through
            case 0: return; // Note: getRGB() is really getARGB()
        }
        final int  maxBase = colors.length-2;
        final double scale = (double)(colors.length-1) / (double)(upper-1-lower);
        for (int i=lower; i<upper; i++) {
            final double index = (i-lower)*scale;
            final int     base = Math.min(maxBase, (int)(index+EPS)); // Round toward 0, which is really what we want.
            final double delta = index-base;
            final Color     C0 = colors[base+0];
            final Color     C1 = colors[base+1];
            int A = C0.getAlpha();
            int R = C0.getRed  ();
            int G = C0.getGreen();
            int B = C0.getBlue ();
            ARGB[i] = (roundByte(A+delta*(C1.getAlpha()-A)) << 24) |
                      (roundByte(R+delta*(C1.getRed  ()-R)) << 16) |
                      (roundByte(G+delta*(C1.getGreen()-G)) <<  8) |
                      (roundByte(B+delta*(C1.getBlue ()-B)) <<  0);
        }
    }

    /**
     * Round a float value and clamp the
     * result between 0 and 255 inclusive.
     */
    public static int roundByte(final double value) {
        return Math.min(Math.max((int)Math.round(value),0),255);
    }

    /**
     * Returns an index color model for specified ARGB codes.   If the specified
     * array has not transparent color (i.e. all alpha values are 255), then the
     * returned color model will be opaque. Otherwise, if the specified array has
     * one and only one color with alpha value of 0, the returned color model will
     * have only this transparent color. Otherwise, the returned color model will
     * be translucent.
     *
     * @param  ARGB An array of ARGB values.
     * @return An index color model for the specified array.
     */
    public static IndexColorModel getIndexColorModel(final int[] ARGB) {
        return getIndexColorModel(ARGB, 1, 0);
    }

    /**
     * Returns a tolerant index color model for the specified ARGB code. This color model accept
     * image with the specified number of bands.
     *
     * @param  ARGB        An array of ARGB values.
     * @param  numBands    The number of bands.
     * @param  visibleBand The band to display.
     * @return An index color model for the specified array.
     *
     * @todo Considerer caching previously created instances using weak references. Index color
     *       model may be big (up to 256 kb), so it may be worth to cache big instances. NOTE:
     *       IndexColorModel inherits a equals(Object) implementation from ColorModel, but do
     *       not override it, so the definition is incomplete.
     */
    public static IndexColorModel getIndexColorModel(final int[] ARGB,
                                                     final int numBands,
                                                     final int visibleBand)
    {
        boolean hasAlpha = false;
        int  transparent = -1;
        final int length = ARGB.length;
        for (int i=0; i<length; i++) {
            final int alpha = (ARGB[i] & 0xFF000000);
            if (alpha != 0xFF000000) {
                if (alpha == 0x00000000 && transparent < 0) {
                    transparent = i;
                    continue;
                }
                hasAlpha = true;
                break;
            }
        }
        final int bits = getBitCount(length);
        final int type = getTransferType(length);
        if (numBands == 1) {
            return new IndexColorModel(bits, length, ARGB, 0, hasAlpha, transparent, type);
        } else {
            return new MultiBandsIndexColorModel(bits, length, ARGB, 0, hasAlpha, transparent,
                                                 type, numBands, visibleBand);
        }
    }

    /**
     * Returns a bit count for an {@link IndexColorModel} mapping {@code mapSize} colors.
     * It is guaranteed that the following relation is hold:
     *
     * <center><pre>(1 << getBitCount(mapSize)) >= mapSize</pre></center>
     */
    public static int getBitCount(final int mapSize) {
        int max = mapSize - 1;
        if (max <= 1) {
            return 1;
        }
        int count = 0;
        do {
            count++;
            max >>= 1;
        } while (max != 0);
        assert (1 << count) >= mapSize : mapSize;
        assert (1 << (count-1)) < mapSize : mapSize;
        return count;
    }

    /**
     * Returns a suggered type for an {@link IndexColorModel}
     * of {@code mapSize} colors. This method returns
     * {@link DataBuffer#TYPE_BYTE} or {@link DataBuffer#TYPE_USHORT}.
     */
    public static int getTransferType(final int mapSize) {
        return (mapSize <= 256) ? DataBuffer.TYPE_BYTE : DataBuffer.TYPE_USHORT;
    }

    /**
     * Transforms a color from XYZ color space to LAB. The color are transformed
     * in place. This method returns {@code color} for convenience.
     * Reference: http://www.brucelindbloom.com/index.html?ColorDifferenceCalc.html
     */
    public static float[] XYZtoLAB(final float[] color) {
        color[0] /= 0.9642;   // Other refeference: 0.95047;
        color[1] /= 1.0000;   //                    1.00000;
        color[2] /= 0.8249;   //                    1.08883;
        for (int i=0; i<3; i++) {
            final float c = color[i];
            color[i] = (float)((c > 216/24389f) ? Math.pow(c, 1.0/3) : ((24389/27.0)*c + 16)/116);
        }
        final float L = 116 *  color[1] - 16;
        final float a = 500 * (color[0] - color[1]);
        final float b = 200 * (color[1] - color[2]);
        assert !Float.isNaN(L) && !Float.isNaN(a) && !Float.isNaN(b);
        color[0] = L;
        color[1] = a;
        color[2] = b;
        return color;
    }

    /**
     * Computes the distance E (CIE 1994) between two colors in LAB color space.
     * Reference: http://www.brucelindbloom.com/index.html?ColorDifferenceCalc.html
     */
    public static float colorDistance(final float[] lab1, final float[] lab2) {
        if (false) {
            // Compute distance using CIE94 formula.
            // NOTE: this formula sometime fails because of negative
            //       value in the first Math.sqrt(...) expression.
            final double dL = (double)lab1[0] - lab2[0];
            final double da = (double)lab1[1] - lab2[1];
            final double db = (double)lab1[2] - lab2[2];
            final double C1 = XMath.hypot(lab1[1], lab1[2]);
            final double C2 = XMath.hypot(lab2[1], lab2[2]);
            final double dC = C1 - C2;
            final double dH = Math.sqrt(da*da + db*db - dC*dC);
            final double sL = dL / 2;
            final double sC = dC / (1 + 0.048*C1);
            final double sH = dH / (1 + 0.014*C1);
            return (float)Math.sqrt(sL*sL + sC*sC + sH*sH);
        } else {
            // Compute distance using delta E formula.
            double sum = 0;
            for (int i=Math.min(lab1.length, lab2.length); --i>=0;) {
                final double delta = lab1[i] - lab2[i];
                sum += delta*delta;
            }
            return (float)Math.sqrt(sum);
        }
    }

    /**
     * Returns the most transparent pixel in the specified color model. If many colors has
     * the same alpha value, than the darkest one is returned. This method never returns
     * a negative value (0 is returned if the color model has no colors).
     *
     * @param  colors The color model in which to look for a transparent color.
     * @return The index of a transparent color, or 0.
     */
    public static int getTransparentPixel(final IndexColorModel colors) {
        int index = colors.getTransparentPixel();
        if (index < 0) {
            index = 0;
            int   alpha = Integer.MAX_VALUE;
            float delta = Float.POSITIVE_INFINITY;
            final ColorSpace space = colors.getColorSpace();
            final float[] RGB   = new float[3];
            final float[] BLACK = XYZtoLAB(space.toCIEXYZ(RGB)); // Black in Lab color space.
            assert BLACK != RGB;
            for (int i=colors.getMapSize(); --i>=0;) {
                final int a = colors.getAlpha(i);
                if (a <= alpha) {
                    RGB[0] = colors.getRed  (i)/255f;
                    RGB[1] = colors.getGreen(i)/255f;
                    RGB[2] = colors.getBlue (i)/255f;
                    final float d = colorDistance(XYZtoLAB(space.toCIEXYZ(RGB)), BLACK);
                    assert d >= 0 : i; // Check mostly for NaN value
                    if (a<alpha || d<delta) {
                        alpha = a;
                        delta = d;
                        index = i;
                    }
                }
            }
        }
        return index;
    }

    /**
     * Returns the index of the specified color, excluding the specified one. If the color
     * is not explicitly found, a close color is returned. This method never returns a negative
     * value (0 is returned if the color model has no colors).
     *
     * @param  colors The color model in which to look for a color index.
     * @param  color The color to search for.
     * @param  exclude An index to exclude from the search (usually the background or the
     *         {@linkplain #getTransparentPixel transparent} pixel), or -1 if none.
     * @return The index of the color, or 0.
     */
    public static int getColorIndex(final IndexColorModel colors,
                                    final Color color,
                                    final int exclude)
    {
        final ColorSpace space = colors.getColorSpace();
        final float[] RGB = {
            color.getRed()  /255f,
            color.getGreen()/255f,
            color.getBlue() /255f
        };
        final float[] REF = XYZtoLAB(space.toCIEXYZ(RGB));
        float delta = Float.POSITIVE_INFINITY;
        int index = 0;
        assert REF != RGB;
        for (int i=colors.getMapSize(); --i>=0;) {
            if (i != exclude) {
                RGB[0] = colors.getRed  (i)/255f;
                RGB[1] = colors.getGreen(i)/255f;
                RGB[2] = colors.getBlue (i)/255f;
                final float d = colorDistance(XYZtoLAB(space.toCIEXYZ(RGB)), REF);
                assert d >= 0 : i; // Check mostly for NaN value
                if (d <= delta) {
                    delta = d;
                    index = i;
                }
            }
        }
        return index;
    }

    /**
     * Tries to guess the number of bands from the specified color model. The recommanded approach
     * is to invoke {@link java.awt.image.SampleModel#getNumBands}. This method should be used only
     * as a fallback when the sample model is not available. This method uses some heuristic rules
     * for guessing the number of bands, so the return value may not be exact in all cases.
     */
    public static int getNumBands(final ColorModel model) {
        if (model instanceof IndexColorModel) {
            if (model instanceof MultiBandsIndexColorModel) {
                return ((MultiBandsIndexColorModel) model).numBands;
            }
            return 1;
        }
        return model.getNumComponents();
    }

    /**
     * Tells us if a specific {@link IndexColorModel} contains only gray color
     * or not, ignoring alpha information.
     *
     * @param  icm {@link IndexColorModel} to be inspected.
     * @param  ignoreTransparents {@code true} if the RGB values of fully transparent pixels
     *         (the ones with an {@linkplain IndexColorModel#getAlpha(int) alpha} value of 0)
     *         should not be taken in account during the check for gray color.
     * @return {@code true} if the palette is grayscale, {@code false} otherwise.
     */
    public static boolean isGrayPalette(final IndexColorModel icm, boolean ignoreTransparents) {
        if (!icm.hasAlpha()) {
            // We will not check transparent pixels if there is none in the color model.
            ignoreTransparents = false;
        }
        final int mapSize = icm.getMapSize();
        for (int i=0; i<mapSize; i++) {
            if (ignoreTransparents) {
                // If this entry is transparent and we were asked
                // to check transparents pixels, let's leave.
                final int a = icm.getAlpha(i);
                if (a == 0) {
                    continue;
                }
            }
            // get the color for this pixel including the
            // alpha information only if it is requested.
            final int r = icm.getRed  (i);
            final int g = icm.getGreen(i);
            final int b = icm.getBlue (i);
            // If gray, all components are the same.
            if (r != g || g != b) {
                return false;
            }
        }
        return true;
    }
}
