/*
 *    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.image.*;
import java.awt.Dimension;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.io.IOException;
import java.io.FileNotFoundException;
import javax.imageio.IIOException;
import javax.imageio.ImageTypeSpecifier;
import javax.swing.JFrame;

import org.geotools.resources.Utilities;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.i18n.ErrorKeys;


/**
 * A set of RGB colors created by a {@linkplain PaletteFactory palette factory} from
 * a {@linkplain #name}. A palette can creates a {@linkplain ColorModel color model}
 * (often {@linkplain IndexColorModel indexed}) 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 kilobytes.
 * 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/Palette.java $
 * @version $Id: Palette.java 26708 2007-08-27 19:42:59Z desruisseaux $
 * @author Antoine Hnawia
 * @author Martin Desruisseaux
 */
public abstract class Palette {
    /**
     * The originating factory.
     */
    final PaletteFactory factory;

    /**
     * The name of this palette.
     */
    protected final String name;

    /**
     * The number of bands in the {@linkplain ColorModel color model}.
     * The value is 1 in the vast majority of cases.
     */
    protected final int numBands;

    /**
     * The band to display, in the range 0 inclusive to {@link #numBands} exclusive.
     * This is used when an image contains more than one band but only one band can
     * be used for computing the colors to display. For example {@link IndexColorModel}
     * works on only one band.
     */
    protected final int visibleBand;

    /**
     * The sample model to be given to {@link ImageTypeSpecifier}.
     */
    private transient SampleModel samples;

    /**
     * A weak reference to the color model. This color model may consume a significant
     * amount of memory (up to 256 kb). Consequently, we will prefer {@link WeakReference}
     * over {@link java.lang.ref.SoftReference}. The purpose of this weak reference is to
     * share existing instances, not to cache it since it is cheap to rebuild.
     */
    private transient Reference/*<ColorModel>*/ colors;

    /**
     * A weak reference to the image specifier to be returned by {@link #getImageTypeSpecifier}.
     * We use weak reference because the image specifier contains a reference to the color model
     * and we don't want to prevent it to be garbage collected. See {@link #colors} for an
     * explanation about why we use weak instead of soft references.
     */
    private transient Reference/*<ImageTypeSpecifier>*/ specifier;

    /**
     * Creates a palette with the specified name.
     *
     * @param factory     The originating factory.
     * @param name        The palette name.
     * @param numBands    The number of bands (usually 1) to assign to {@link #numBands}.
     * @param visibleBand The visible band (usually 0) to assign to {@link #visibleBand}.
     */
    protected Palette(final PaletteFactory factory, final String name,
                      final int numBands, final int visibleBand)
    {
        if (factory == null) {
            // Can't use factory.getErrorResources() here.
            throw new IllegalArgumentException(Errors.format(
                    ErrorKeys.NULL_ARGUMENT_$1, "factory"));
        }
        if (name == null) {
            throw new IllegalArgumentException(factory.getErrorResources().getString(
                    ErrorKeys.NULL_ARGUMENT_$1, "name"));
        }
        ensureInsideBounds(numBands, 0, 255); // This maximal value is somewhat arbitrary.
        ensureInsideBounds(visibleBand, 0, numBands-1);
        this.factory     = factory;
        this.name        = name.trim();
        this.numBands    = numBands;
        this.visibleBand = visibleBand;
    }

    /**
     * Ensures that the specified values in inside the expected bounds (inclusives).
     *
     * @throws IllegalArgumentException if the specified values are outside the bounds.
     */
    final void ensureInsideBounds(final int value, final int min, final int max)
            throws IllegalArgumentException
    {
        if (value < min || value > max) {
            throw new IllegalArgumentException(factory.getErrorResources().getString(
                    ErrorKeys.VALUE_OUT_OF_BOUNDS_$3,
                    new Integer(value), new Integer(min), new Integer(max)));
        }
    }

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

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

    /**
     * Returns the color model 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 {@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.
     */
    public synchronized ColorModel getColorModel() throws IOException {
        if (colors != null) {
            final ColorModel candidate = (ColorModel) colors.get();
            if (candidate != null) {
                return candidate;
            }
        }
        return getImageTypeSpecifier().getColorModel();
    }

    /**
     * Returns the image type specifier for this palette.
     *
     * @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.
     */
    public abstract ImageTypeSpecifier getImageTypeSpecifier() throws IOException;

    /**
     * Returns the image type specifier from the cache, or {@code null}.
     */
    final ImageTypeSpecifier queryCache() {
        if (specifier != null) {
            final ImageTypeSpecifier candidate = (ImageTypeSpecifier) specifier.get();
            if (candidate != null) {
                return candidate;
            }
        }
        if (samples!=null && colors!=null) {
            final ColorModel candidate = (ColorModel) colors.get();
            if (candidate != null) {
                final ImageTypeSpecifier its = new ImageTypeSpecifier(candidate, samples);
                specifier = new WeakReference(its);
                return its;
            }
        }
        return null;
    }

    /**
     * Puts the specified image specifier in the cache.
     */
    final void cache(final ImageTypeSpecifier its) {
        samples   = its.getSampleModel();
        colors    = new PaletteDisposer.Reference(this, its.getColorModel());
        specifier = new WeakReference/*<ImageTypeSpecifier>*/(its);
    }

    /**
     * Returns the color palette as an image of the specified size.
     * This is useful for looking visually at a color palette.
     *
     * @param size The image size. The palette will be vertical if
     *        <code>size.{@linkplain Dimension#height height}</code> &gt;
     *        <code>size.{@linkplain Dimension#width  width }</code>
     *
     * @throws IOException if the color values can't be read.
     */
    public RenderedImage getImage(final Dimension size) throws IOException {
        final IndexColorModel colors;
        final BufferedImage   image;
        final WritableRaster  raster;
        colors = (IndexColorModel) getColorModel();
        raster = colors.createCompatibleWritableRaster(size.width, size.height);
        image  = new BufferedImage(colors, raster, false, null);
        int xmin   = raster.getMinX();
        int ymin   = raster.getMinY();
        int width  = raster.getWidth();
        int height = raster.getHeight();
        final boolean horizontal = size.width >= size.height;
        // Computation will be performed as if the image were horizontal.
        // If it is not, interchanges x and y values.
        if (!horizontal) {
            int tmp;
            tmp = xmin;  xmin  = ymin;   ymin   = tmp;
            tmp = width; width = height; height = tmp;
        }
        final int xmax = xmin + width;
        final int ymax = ymin + height;
        final double scale  = getScale() / width;
        final double offset = getOffset();
        for (int x=xmin; x<xmax; x++) {
            final double value = offset + scale*(x-xmin);
            for (int y=ymin; y<ymax; y++) {
                if (horizontal) {
                    raster.setSample(x, y, 0, value);
                } else {
                    raster.setSample(y, x, 0, value);
                }
            }
        }
        return image;
    }

    /**
     * Shows the palette in a windows. This is mostly for debugging purpose.
     *
     * @throws IOException if the color values can't be read.
     */
    public void show() throws IOException {
        final JFrame frame = new JFrame(toString());
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.add(new javax.media.jai.widget.ImageCanvas(getImage(new Dimension(256, 32))));
        frame.pack();
        frame.setVisible(true);
    }

    /**
     * Returns a hash value for this palette.
     */
    //@Override
    public int hashCode() {
        return name.hashCode() + 37*numBands + 17*visibleBand;
    }

    /**
     * Compares this palette with the specified object for equality.
     */
    //@Override
    public boolean equals(final Object object) {
        if (object != null && getClass().equals(object.getClass())) {
            final Palette that = (Palette) object;
            return this.numBands    == that.numBands    &&
                   this.visibleBand == that.visibleBand &&
                   Utilities.equals(this.name, that.name);
            /*
             * Note: we do not compare PaletteFactory on purpose, since two instances could be
             * identical except for the locale to use for formatting error messages.   Because
             * Palettes are used as keys in the PaletteFactory.palettes pool, we don't want to
             * get duplicated palettes only because they format error messages differently.
             */
        }
        return false;
    }
}
