/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2006, GeoTools Project Managment Committee (PMC)
 *    (C) 2005, Institut de Recherche pour le Développement
 *    (C) 2006, Geomatys
 *   
 *    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;
 *    version 2.1 of the License.
 *
 *    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.image.io;

import java.awt.Color;
import java.awt.image.*;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.IIOException;
import java.io.FileNotFoundException;
import java.io.IOException;

import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.image.ColorUtilities;


/**
 * A set of RGB colors created by a {@linkplain PaletteFactory palette factory} from a name.
 * A palette can creates an {@linkplain IndexColorModel index color model} or an {@linkplain
 * ImageTypeSpecifier image type specifier} from the RGB colors. The color model is retained
 * by the palette as a {@linkplain WeakReference weak reference} (<strong>not</strong> as a
 * {@linkplain java.lang.ref.SoftReference soft reference}) because it may consume up to 256
 * kb. The purpose of the weak reference is to share existing instances in order to reduce
 * memory usage; the purpose is not to provide caching.
 *
 * @since 2.4
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/unsupported/coverageio/src/main/java/org/geotools/image/io/IndexedPalette.java $
 * @version $Id: IndexedPalette.java 26708 2007-08-27 19:42:59Z desruisseaux $
 * @author Antoine Hnawia
 * @author Martin Desruisseaux
 */
final class IndexedPalette extends Palette {
    /**
     * The maximal allowed value, corresponding to the maximum value for unsigned 16 bits integer.
     * DO NOT EDIT: this value <strong>MUST</strong> be {@code 0xFFFF}.
     */
    private static final int MAX_UNSIGNED = 0xFFFF;

    /**
     * Index of the first valid element (inclusive) in the {@linkplain IndexColorModel
     * index color model} to be created. Pixels in the range 0 inclusive to {@code lower}
     * exclusive will be reserved for "no data" values.
     * <p>
     * Strictly speaking, this index should be non-negative because {@link IndexColorModel}
     * do not supports negative index. However this {@code Palette} implementation accepts
     * negative values provided that {@link #upper} is not greater than {@value Short#MAX_VALUE}.
     * If this condition holds, then {@code Palette} will transpose negative values as positive
     * values in the range {@code 0x80000} to {@code 0xFFFF} inclusive. Be aware that such
     * approach consume the maximal amount of memory, i.e. 256 kilobytes for each color model.
     */
    protected final int lower;

    /**
     * Index of the last valid element (exclusive) in the {@linkplain IndexColorModel
     * index color model} to be created. Pixels in the range {@code upper} inclusive
     * to {@link #size} exclusive will be reserved for "no data" values. This value
     * is always greater than {@link #lower} (note that it may be negative).
     */
    protected final int upper;

    /**
     * The size of the {@linkplain IndexColorModel index color model} to be created.
     * This is the value to be returned by {@link IndexColorModel#getMapSize}. This
     * value is always positive.
     */
    protected final int size;

    /**
     * Creates a palette with the specified name and size. The RGB colors will be distributed
     * in the range {@code lower} inclusive to {@code upper} exclusive. Remaining pixel values
     * (if any) will be left to a black or transparent color by default.
     *
     * @param factory The originating factory.
     * @param name    The palette name.
     * @param lower   Index of the first valid element (inclusive) in the
     *                {@linkplain IndexColorModel index color model} to be created.
     * @param upper   Index of the last valid element (exclusive) in the
     *                {@linkplain IndexColorModel index color model} to be created.
     * @param size    The size of the {@linkplain IndexColorModel index color model} to be created.
     *                This is the value to be returned by {@link IndexColorModel#getMapSize}.
     * @param numBands    The number of bands (usually 1).
     * @param visibleBand The band to use for color computations (usually 0).
     */
    protected IndexedPalette(final PaletteFactory factory, final String name, final int lower,
                   final int upper, int size, final int numBands, final int visibleBand)
    {
        super(factory, name, numBands, visibleBand);
        final int minAllowed, maxAllowed; // inclusives
        if (lower < 0) {
            minAllowed = Short.MIN_VALUE;
            maxAllowed = Short.MAX_VALUE;
            size       = (size <= 0x100) ? 0x100 : (MAX_UNSIGNED + 1);
            // 'size' must be FF or FFFF in order to rool negative values.
        } else {
            minAllowed = 0;
            maxAllowed = MAX_UNSIGNED;
        }
        ensureInsideBounds(lower, minAllowed, maxAllowed);
        ensureInsideBounds(upper, minAllowed, maxAllowed + 1);
        ensureInsideBounds(size,  upper,      MAX_UNSIGNED + 1);
        if (lower >= upper) {
            throw new IllegalArgumentException(factory.getErrorResources().getString(
                    ErrorKeys.BAD_RANGE_$2, new Integer(lower), new Integer(upper)));
        }
        this.lower = lower;
        this.upper = upper;
        this.size  = size;
    }

    /**
     * Returns the scale from <cite>normalized values</cite> (values in the range [0..1])
     * to values in the range of this palette.
     */
    //@Override
    double getScale() {
        return upper - lower;
    }

    /**
     * Returns the offset from <cite>normalized values</cite> (values in the range [0..1])
     * to values in the range of this palette.
     */
    //@Override
    double getOffset() {
        return lower;
    }

    /**
     * Creates and returns ARGB values for the {@linkplain IndexColorModel index color model} to be
     * created. This method is invoked automatically the first time the color model is required, or
     * when it need to be rebuilt.
     *
     * @throws  FileNotFoundException If the RGB values need to be read from a file and this file
     *                                (typically inferred from {@link #name}) is not found.
     * @throws  IOException           If an other find of I/O error occured.
     * @throws  IIOException          If an other kind of error prevent this method to complete.
     */
    private int[] createARGB() throws IOException {
        final Color[] colors = factory.getColors(name);
        if (colors == null) {
            throw new FileNotFoundException(factory.getErrorResources().getString(
                    ErrorKeys.FILE_DOES_NOT_EXIST_$1, name));
        }
        final int[] ARGB = new int[size];
        if (lower >= 0) {
            ColorUtilities.expand(colors, ARGB, lower, upper);
        } else {
            ColorUtilities.expand(colors, ARGB, 0, upper - lower);
            final int negativeStart = size + lower;
            final int negativeCount = -lower;
            final int[] negatives = new int[negativeCount];
            System.arraycopy(ARGB, 0, negatives, 0, negativeCount);
            System.arraycopy(ARGB, negativeCount, ARGB, 0, negativeStart);
            System.arraycopy(negatives, 0, ARGB, negativeStart, negativeCount);
        }
        return ARGB;
    }

    /**
     * Returns the image type specifier for this palette. This method tries to reuse existing
     * color model if possible, since it may consume a significant amount of memory.
     *
     * @throws  FileNotFoundException If the RGB values need to be read from a file and this file
     *          (typically inferred from {@linkplain #name name}) is not found.
     * @throws  IOException  If an other find of I/O error occured.
     * @throws  IIOException If an other kind of error prevent this method to complete.
     */
    public synchronized ImageTypeSpecifier getImageTypeSpecifier() throws IOException {
        /*
         * First checks the weak references.
         */
        ImageTypeSpecifier its = queryCache();
        if (its != null) {
            return its;
        }
        /*
         * Nothing reacheable. Rebuild the specifier.
         */
        final int[] ARGB = createARGB();
        final int bits = ColorUtilities.getBitCount(ARGB.length);
        final int type = (bits <= 8) ? DataBuffer.TYPE_BYTE : DataBuffer.TYPE_USHORT;
        final boolean packed = (bits==1 || bits==2 || bits==4);
        final boolean dense  = (packed || bits==8 || bits==16);
        if (dense && (1 << bits) == ARGB.length && numBands == 1) {
            final byte[] A = new byte[ARGB.length];
            final byte[] R = new byte[ARGB.length];
            final byte[] G = new byte[ARGB.length];
            final byte[] B = new byte[ARGB.length];
            for (int i=0; i<ARGB.length; i++) {
                int code = ARGB[i];
                B[i] = (byte) ((code       ) & 0xFF);
                G[i] = (byte) ((code >>>= 8) & 0xFF);
                R[i] = (byte) ((code >>>= 8) & 0xFF);
                A[i] = (byte) ((code >>>= 8) & 0xFF);
            }
            its = ImageTypeSpecifier.createIndexed(R,G,B,A, bits, type);
        } else {
            /*
             * The "ImageTypeSpecifier.createIndexed(...)" method is too strict. The IndexColorModel
             * constructor is more flexible. This block mimic the "ImageTypeSpecifier.createIndexed"
             * work without the constraints imposed by "createIndexed". Being more flexible consume
             * less memory for the color palette, since we don't force it to be 64 kb in the USHORT
             * data type case.
             */
            final IndexColorModel colors = ColorUtilities.getIndexColorModel(ARGB, numBands, visibleBand);
            final SampleModel samples;
            if (packed) {
                samples = new MultiPixelPackedSampleModel(type, 1, 1, bits);
            } else {
                samples = new PixelInterleavedSampleModel(type, 1, 1, 1, 1, new int[1]);
            }
            its = new ImageTypeSpecifier(colors, samples);
        }
        cache(its);
        return its;
    }

    /**
     * Returns a hash value for this palette.
     */
    //@Override
    public int hashCode() {
        return super.hashCode() + 37*(lower + 37*(upper + 37*size));
    }

    /**
     * Compares this palette with the specified object for equality.
     */
    //@Override
    public boolean equals(final Object object) {
        if (object == this) {
            return true;
        }
        if (super.equals(object)) {
            final IndexedPalette that = (IndexedPalette) object;
            return this.lower == that.lower &&
                   this.upper == that.upper &&
                   this.size  == that.size;
        }
        return false;
    }

    /**
     * Returns a string representation of this palette. Used for debugging purpose only.
     */
    //@Override
    public String toString() {
        return name + " [" + lower + "..." + (upper-1) + "] size=" + size;
    }
}
