/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2007, Geotools Project Managment Committee (PMC)
 *    (C) 2007, 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; 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.image.io;

import java.util.Set;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Collections;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.LogRecord;

import java.awt.Rectangle;
import java.awt.image.ColorModel;      // For javadoc
import java.awt.image.DataBuffer;
import java.awt.image.SampleModel;     // For javadoc
import java.awt.image.BufferedImage;
import java.awt.image.IndexColorModel; // For javadoc

import java.io.IOException;
import javax.imageio.ImageReader;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.event.IIOReadWarningListener; // For javadoc

import org.geotools.util.NumberRange;
import org.geotools.util.logging.Logging;
import org.geotools.resources.XArray;
import org.geotools.resources.i18n.Locales;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.IndexedResourceBundle;
import org.geotools.image.io.metadata.GeographicMetadata;
import org.geotools.image.io.metadata.Band;


/**
 * Base class for readers of geographic images. The default implementation assumes that only one
 * {@linkplain ImageTypeSpecifier image type} is supported (as opposed to the arbitrary number
 * allowed by the standard {@link ImageReader}). It also provides a default image type built
 * automatically from a color palette and a range of valid values.
 * <p>
 * More specifically, this class provides the following conveniences to implementors:
 *
 * <ul>
 *   <li><p>Provides default {@link #getNumImages} and {@link #getNumBands} implementations,
 *       which return 1. This default behavior matches simple image formats like flat binary
 *       files or ASCII files. Those methods need to be overrided for more complex image
 *       formats.</p></li>
 *
 *   <li><p>Provides {@link #checkImageIndex} and {@link #checkBandIndex} convenience methods.
 *       Those methods are invoked by most implementation of public methods. They perform their
 *       checks based on the informations provided by the above-cited {@link #getNumImages} and
 *       {@link #getNumBands} methods.</p></li>
 *
 *   <li><p>Provides default implementations of {@link #getImageTypes} and {@link #getRawImageType},
 *       which assume that only one {@linkplain ImageTypeSpecifier image type} is supported. The
 *       default image type is created from the informations provided by {@link #getRawDataType}
 *       and {@link #getImageMetadata}.</p></li>
 *
 *   <li><p>Provides {@link #getStreamMetadata} and {@link #getImageMetadata} default
 *       implementations, which return {@code null} as authorized by the specification.
 *       Note that subclasses should consider returning {@link GeographicMetadata} instances.</p></li>
 * </ul>
 *
 * Images may be flat binary or ASCII files with no meta-data and no color information.
 * Their pixel values may be floating point values instead of integers. The default
 * implementation assumes floating point values and uses a grayscale color space scaled
 * to fit the range of values. Displaying such an image may be very slow. Consequently,
 * users who want to display image are encouraged to change data type and color space with
 * <a href="http://java.sun.com/products/java-media/jai/">Java Advanced Imaging</a>
 * operators after reading.
 *
 * @since 2.4
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/unsupported/coverageio/src/main/java/org/geotools/image/io/GeographicImageReader.java $
 * @version $Id: GeographicImageReader.java 27862 2007-11-12 19:51:19Z desruisseaux $
 * @author Martin Desruisseaux
 */
public abstract class GeographicImageReader extends ImageReader {
    /**
     * The logger to use for events related to this image reader.
     */
    static final Logger LOGGER = Logging.getLogger("org.geotools.image.io");

    /**
     * Metadata for each images, or {@code null} if not yet created.
     */
    private transient GeographicMetadata[] metadata;

    /**
     * Constructs a new image reader.
     *
     * @param provider The {@link ImageReaderSpi} that is invoking this constructor,
     *        or {@code null} if none.
     */
    protected GeographicImageReader(final ImageReaderSpi provider) {
        super(provider);
        availableLocales = Locales.getAvailableLocales();
    }

    /**
     * Sets the input source to use.
     *
     * @param input           The input object to use for future decoding.
     * @param seekForwardOnly If {@code true}, images and metadata may only be read
     *                        in ascending order from this input source.
     * @param ignoreMetadata  If {@code true}, metadata may be ignored during reads.
     */
    //@Override
    public void setInput(final Object  input,
                         final boolean seekForwardOnly,
                         final boolean ignoreMetadata)
    {
        metadata = null; // Clears the cache
        super.setInput(input, seekForwardOnly, ignoreMetadata);
    }

    /**
     * Returns the resources for formatting error messages.
     */
    final IndexedResourceBundle getErrorResources() {
        return Errors.getResources(getLocale());
    }

    /**
     * Ensures that the specified image index is inside the expected range.
     * The expected range is {@link #minIndex minIndex} inclusive (initially 0)
     * to <code>{@link #getNumImages getNumImages}(false)</code> exclusive.
     *
     * @param  imageIndex Index to check for validity.
     * @throws IndexOutOfBoundsException if the specified index is outside the expected range.
     * @throws IOException If the operation failed because of an I/O error.
     */
    protected void checkImageIndex(final int imageIndex)
            throws IOException, IndexOutOfBoundsException
    {
        final int numImages = getNumImages(false);
        if (imageIndex < minIndex || (imageIndex >= numImages && numImages >= 0)) {
            throw new IndexOutOfBoundsException(indexOutOfBounds(imageIndex, minIndex, numImages));
        }
    }

    /**
     * Ensures that the specified band index is inside the expected range. The expected
     * range is 0 inclusive to <code>{@link #getNumBands getNumBands}(imageIndex)</code>
     * exclusive.
     *
     * @param  imageIndex The image index.
     * @param  bandIndex Index to check for validity.
     * @throws IndexOutOfBoundsException if the specified index is outside the expected range.
     * @throws IOException If the operation failed because of an I/O error.
     */
    protected void checkBandIndex(final int imageIndex, final int bandIndex)
            throws IOException, IndexOutOfBoundsException
    {
        // Call 'getNumBands' first in order to call 'checkImageIndex'.
        final int numBands = getNumBands(imageIndex);
        if (bandIndex >= numBands || bandIndex < 0) {
            throw new IndexOutOfBoundsException(indexOutOfBounds(bandIndex, 0, numBands));
        }
    }

    /**
     * Formats an error message for an index out of bounds.
     *
     * @param index The index out of bounds.
     * @param lower The lower legal value, inclusive.
     * @param upper The upper legal value, exclusive.
     */
    private String indexOutOfBounds(final int index, final int lower, final int upper) {
        return getErrorResources().getString(ErrorKeys.VALUE_OUT_OF_BOUNDS_$3,
                new Integer(index), new Integer(lower), new Integer(upper-1));
    }

    /**
     * Returns the number of images available from the current input source.
     * The default implementation returns 1.
     *
     * @param  allowSearch If true, the number of images will be returned
     *         even if a search is required.
     * @return The number of images, or -1 if {@code allowSearch}
     *         is false and a search would be required.
     *
     * @throws IllegalStateException if the input source has not been set.
     * @throws IOException if an error occurs reading the information from the input source.
     */
    public int getNumImages(final boolean allowSearch) throws IllegalStateException, IOException {
        if (input != null) {
            return 1;
        }
        throw new IllegalStateException(getErrorResources().getString(ErrorKeys.NO_IMAGE_INPUT));
    }

    /**
     * Returns the number of bands available for the specified image.
     * The default implementation returns 1.
     *
     * @param  imageIndex  The image index.
     * @throws IOException if an error occurs reading the information from the input source.
     */
    public int getNumBands(final int imageIndex) throws IOException {
        checkImageIndex(imageIndex);
        return 1;
    }

    /**
     * Returns metadata associated with the input source as a whole. Since many raw images
     * can't store metadata, the default implementation returns {@code null}.
     *
     * @throws IOException if an error occurs during reading.
     */
    public IIOMetadata getStreamMetadata() throws IOException {
        return null;
    }

    /**
     * Returns metadata associated with the given image. Since many raw images
     * can't store metadata, the default implementation returns {@code null}.
     *
     * @param  imageIndex The image index.
     * @return The metadata, or {@code null} if none.
     * @throws IOException if an error occurs during reading.
     */
    public IIOMetadata getImageMetadata(int imageIndex) throws IOException {
        checkImageIndex(imageIndex);
        return null;
    }

    /**
     * Returns a helper parser for metadata associated with the given image. This implementation
     * invokes  <code>{@linkplain #getImageMetadata getImageMetadata}(imageIndex)</code>,  wraps
     * the result in a {@link GeographicMetadata} object if non-null and caches the result.
     * <p>
     * Note that this method forces {@link #ignoreMetadata} to {@code false} for the time of
     * <code>{@linkplain #getImageMetadata getImageMetadata}(imageIndex)</code> execution,
     * because some image reader implementations need geographic metadata in order to infer
     * a valid {@linkplain ColorModel color model}.
     *
     * @param  imageIndex The image index.
     * @return The geographic metadata, or {@code null} if none.
     * @throws IOException if an error occurs during reading.
     */
    public GeographicMetadata getGeographicMetadata(final int imageIndex) throws IOException {
        // Checks if a cached instance is available.
        if (metadata != null && imageIndex >= 0 && imageIndex < metadata.length) {
            final GeographicMetadata parser = metadata[imageIndex];
            if (parser != null) {
                return parser;
            }
        }
        // Checks if metadata are availables. If the user set 'ignoreMetadata' to 'true',
        // we override his setting since we really need metadata for creating a ColorModel.
        final IIOMetadata candidate;
        final boolean oldIgnore = ignoreMetadata;
        try {
            ignoreMetadata = false;
            candidate = getImageMetadata(imageIndex);
        } finally {
            ignoreMetadata = oldIgnore;
        }
        if (candidate == null) {
            return null;
        }
        // Wraps the IIOMetadata into a GeographicMetadata object,
        // if it was not already of the appropriate type.
        final GeographicMetadata parser;
        if (candidate instanceof GeographicMetadata) {
            parser = (GeographicMetadata) candidate;
        } else {
            parser = new GeographicMetadata(this);
            parser.mergeTree(candidate);
        }
        if (metadata == null) {
            metadata = new GeographicMetadata[Math.max(imageIndex+1, 4)];
        }
        if (imageIndex >= metadata.length) {
            metadata = (GeographicMetadata[]) XArray.resize(metadata, Math.max(imageIndex+1, metadata.length*2));
        }
        metadata[imageIndex] = parser;
        return parser;
    }

    /**
     * Returns a collection of {@link ImageTypeSpecifier} containing possible image types to which
     * the given image may be decoded. The default implementation returns a singleton containing
     * <code>{@link #getRawImageType(int) getRawImageType}(imageIndex)</code>.
     *
     * @param  imageIndex The index of the image to be retrieved.
     * @return A set of suggested image types for decoding the current given image.
     * @throws IOException If an error occurs reading the format information from the input source.
     */
    public Iterator getImageTypes(final int imageIndex) throws IOException {
        return Collections.singleton(getRawImageType(imageIndex)).iterator();
    }

    /**
     * Returns an image type specifier indicating the {@link SampleModel} and {@link ColorModel}
     * which most closely represents the "raw" internal format of the image. The default
     * implementation delegates to the following:
     *
     * <blockquote><code>{@linkplain #getRawImageType(int,ImageReadParam,SampleConverter[])
     * getRawImageType}(imageIndex, {@linkplain #getDefaultReadParam()}, null);</code></blockquote>
     *
     * If this method needs to be overriden, consider overriding the later instead.
     *
     * @param  imageIndex The index of the image to be queried.
     * @return The image type (never {@code null}).
     * @throws IOException If an error occurs reading the format information from the input source.
     */
    //@Override
    public ImageTypeSpecifier getRawImageType(final int imageIndex) throws IOException {
        return getRawImageType(imageIndex, getDefaultReadParam(), null);
    }

    /**
     * Returns an image type specifier indicating the {@link SampleModel} and {@link ColorModel}
     * which most closely represents the "raw" internal format of the image. The default
     * implementation applies the following rules:
     *
     * <ol>
     *   <li><p>The {@linkplain Band#getValidRange range of expected values} and the
     *       {@linkplain Band#getNoDataValues no-data values} are extracted from the
     *       {@linkplain #getGeographicMetadata geographic metadata}, if any.</p></li>
     *
     *   <li><p>If the given {@code parameters} argument is an instance of {@link GeographicImageReadParam},
     *       then the user-supplied {@linkplain GeographicImageReadParam#getPaletteName palette name}
     *       is fetched. Otherwise or if no palette name was explicitly set, then this method default
     *       to {@value org.geotools.image.io.GeographicImageReadParam#DEFAULT_PALETTE_NAME}. The
     *       palette name will be used in order to {@linkplain PaletteFactory#getColors read a
     *       predefined set of colors} (as RGB values) to be given to the
     *       {@linkplain IndexColorModel index color model}.</p></li>
     *
     *   <li><p>If the {@linkplain #getRawDataType raw data type} is {@link DataBuffer#TYPE_FLOAT
     *       TYPE_FLOAT} or {@link DataBuffer#TYPE_DOUBLE TYPE_DOUBLE}, then this method builds
     *       a {@linkplain PaletteFactory#getContinuousPalette continuous palette} suitable for
     *       the range fetched at step 1. The data are assumed <cite>geophysics</cite> values
     *       rather than some packed values. Consequently, the {@linkplain SampleConverter sample
     *       converters} will replace no-data values by {@linkplain Float#NaN NaN} with no other
     *       changes.</p></li>
     *
     *   <li><p>Otherwise, if the {@linkplain #getRawDataType raw data type} is a unsigned integer type
     *       like {@link DataBuffer#TYPE_BYTE TYPE_BYTE} or {@link DataBuffer#TYPE_USHORT TYPE_USHORT},
     *       then this method builds an {@linkplain PaletteFactory#getPalette indexed palette} (i.e. a
     *       palette backed by an {@linkplain IndexColorModel index color model}) with just the minimal
     *       {@linkplain IndexColorModel#getMapSize size} needed for containing fully the range and the
     *       no-data values fetched at step 1. The data are assumed <cite>packed</cite> values rather
     *       than geophysics values. Consequently, the {@linkplain SampleConverter sample converters}
     *       will be the {@linkplain SampleConverter#IDENTITY identity converter} except in the
     *       following cases:
     *       <ul>
     *         <li>The {@linkplain Band#getValidRange range of valid values} is outside the range
     *             allowed by the {@linkplain #getRawDataType raw data type} (e.g. the range of
     *             valid values contains negative integers). In this case, the sample converter
     *             will shift the values to a strictly positive range and replace no-data values
     *             by 0.</li>
     *         <li>At least one {@linkplain Band#getNoDataValues no-data value} is outside the range
     *             of values allowed by the {@linkplain #getRawDataType raw data type}. In this case,
     *             this method will try to only replace the no-data values by 0, without shifting
     *             the valid values if this shift can be avoided.</li>
     *         <li>At least one {@linkplain Band#getNoDataValues no-data value} is far away from the
     *             {@linkplain Band#getValidRange range of valid values} (for example 9999 while
     *             the range of valid values is [0..255]). The meaning of "far away" is determined
     *             by the {@link #collapseNoDataValues collapseNoDataValues} method.</li>
     *       </ul>
     *       </p></li>
     *
     *   <li><p>Otherwise, if the {@linkplain #getRawDataType raw data type} is a signed integer
     *       type like {@link DataBuffer#TYPE_SHORT TYPE_SHORT}, then this method builds an
     *       {@linkplain PaletteFactory#getPalette indexed palette} with the maximal {@linkplain
     *       IndexColorModel#getMapSize size} supported by the raw data type (note that this is
     *       memory expensive - typically 256 kilobytes). Negative values will be stored in their
     *       two's complement binary form in order to fit in the range of positive integers
     *       supported by the {@linkplain IndexColorModel index color model}.</p></li>
     * </ol>
     *
     * <h3>Overriding this method</h3>
     * Subclasses may override this method when a constant color {@linkplain Palette palette} is
     * wanted for all images in a series, for example for all <cite>Sea Surface Temperature</cite>
     * (SST) from the same provider. A constant color palette facilitates the visual comparaison
     * of different images at different time. The example below creates hard-coded objects:
     *
     * <blockquote><code>
     * int minimum    = -2000; // </code>minimal expected value<code><br>
     * int maximum    = +2300; // </code>maximal expected value<code><br>
     * int fillValue  = -9999; // </code>Value for missing data<code><br>
     * String palette = "SST-Nasa";// </code>Named set of RGB colors<code><br>
     * converters[0] = {@linkplain SampleConverter#createOffset(double,double)
     * SampleConverter.createOffset}(1 - minimum, fillValue);<br>
     * return {@linkplain PaletteFactory#getDefault()}.{@linkplain PaletteFactory#getPalettePadValueFirst
     * getPalettePadValueFirst}(paletteName, maximum - minimum).{@linkplain Palette#getImageTypeSpecifier
     * getImageTypeSpecifier}();
     * </code></blockquote>
     *
     * @param imageIndex
     *              The index of the image to be queried.
     * @param parameters
     *              The user-supplied parameters, or {@code null}. Note: we recommand to supply
     *              {@link #getDefaultReadParam} instead of {@code null} since subclasses may
     *              override the later with default values suitable to a particular format.
     * @param  converters
     *              If non-null, an array where to store the converters created by this method.
     *              Those converters should be used by <code>{@linkplain #read(int,ImageReadParam)
     *              read}(imageIndex, parameters)</code> implementations for converting the values
     *              read in the datafile to values acceptable for the underling {@linkplain
     *              ColorModel color model}.
     * @return
     *              The image type (never {@code null}).
     * @throws IOException
     *              If an error occurs while reading the format information from the input source.
     *
     * @see #getRawDataType
     * @see #collapseNoDataValues
     * @see #getDestination(int, ImageReadParam, int, int, SampleConverter[])
     */
    protected ImageTypeSpecifier getRawImageType(final int               imageIndex,
                                                 final ImageReadParam    parameters,
                                                 final SampleConverter[] converters)
            throws IOException
    {
        /*
         * Gets the minimal and maximal values allowed for the target image type.
         * Note that this is meanless for floating point types, so the values in
         * that case are arbitrary.
         *
         * The only integer types that are signed are SHORT (not to be confused with
         * USHORT) and INT. Other types like BYTE and USHORT are treated as unsigned.
         */
        final boolean isFloat;
        final long floor, ceil;
        final int dataType = getRawDataType(imageIndex);
        switch (dataType) {
            case DataBuffer.TYPE_UNDEFINED: // Actually we don't really know what to do for this case...
            case DataBuffer.TYPE_DOUBLE:    // Fall through since we can treat this case as float.
            case DataBuffer.TYPE_FLOAT: {
                isFloat = true;
                floor   = Long.MIN_VALUE;
                ceil    = Long.MAX_VALUE;
                break;
            }
            case DataBuffer.TYPE_INT: {
                isFloat = false;
                floor   = Integer.MIN_VALUE;
                ceil    = Integer.MAX_VALUE;
                break;
            }
            case DataBuffer.TYPE_SHORT: {
                isFloat = false;
                floor   = Short.MIN_VALUE;
                ceil    = Short.MAX_VALUE;
                break;
            }
            default: {
                isFloat = false;
                floor   = 0;
                ceil    = (1L << DataBuffer.getDataTypeSize(dataType)) - 1;
                break;
            }
        }
        /*
         * Extracts all informations we will need from the user-supplied parameters, if any.
         */
        final String paletteName;
        final int[]  sourceBands;
        final int[]  targetBands;
        final int    visibleBand;
        if (parameters != null) {
            sourceBands = parameters.getSourceBands();
            targetBands = parameters.getDestinationBands();
        } else {
            sourceBands = null;
            targetBands = null;
        }
        if (parameters instanceof GeographicImageReadParam) {
            final GeographicImageReadParam geoparam = (GeographicImageReadParam) parameters;
            paletteName = geoparam.getNonNullPaletteName();
            visibleBand = geoparam.getVisibleBand();
        } else {
            paletteName = GeographicImageReadParam.DEFAULT_PALETTE_NAME;
            visibleBand = 0;
        }
        final int numBands;
        if (sourceBands != null) {
            numBands = sourceBands.length;
        } else if (targetBands != null) {
            numBands = targetBands.length;
        } else {
            numBands = getNumBands(imageIndex);
        }
        /*
         * Computes a range of values for all bands, as the union in order to make sure that
         * we can stores every sample values. Also creates SampleConverters in the process.
         * The later is an opportunist action since we gather most of the needed information
         * during the loop.
         */
        NumberRange     allRanges        = null;
        NumberRange     visibleRange     = null;
        SampleConverter visibleConverter = SampleConverter.IDENTITY;
        double          maximumFillValue = 0; // Only in the visible band, and must be positive.
        final GeographicMetadata metadata = getGeographicMetadata(imageIndex);
        if (metadata != null) {
            final int maxBand = metadata.getNumBands();
            for (int i=0; i<numBands; i++) {
                int bandIndex = (sourceBands != null) ? sourceBands[i] : i;
                if (bandIndex < 0 || bandIndex >= maxBand) {
                    warningOccurred("getRawImageType", indexOutOfBounds(bandIndex, 0, maxBand));
                    if (sourceBands != null) {
                        continue; // Next bands may be okay.
                    } else {
                        break; // We are sure that next bands will not be better.
                    }
                }
                final Band band = metadata.getBand(bandIndex);
                final double[] nodataValues = band.getNoDataValues();
                final NumberRange range = band.getValidRange();
                double minimum, maximum;
                if (range != null) {
                    minimum = range.getMinimum();
                    maximum = range.getMaximum();
                    if (!isFloat) {
                        // If the metadata do not contain any information about the range,
                        // treat as if we use the maximal range allowed by the data type.
                        if (minimum == Double.NEGATIVE_INFINITY) minimum = floor;
                        if (maximum == Double.POSITIVE_INFINITY) maximum = ceil;
                    }
                    final double extent = maximum - minimum;
                    if (extent >= 0 && (isFloat || extent <= (ceil - floor))) {
                        allRanges = (allRanges != null) ? (NumberRange) allRanges.union(range) : range;
                    } else {
                        // Use range.getMin/MaxValue() because they may be integers rather than doubles.
                        warningOccurred("getRawImageType", Errors.format(ErrorKeys.BAD_RANGE_$2,
                                range.getMinValue(), range.getMaxValue()));
                        continue;
                    }
                } else {
                    minimum = Double.NaN;
                    maximum = Double.NaN;
                }
                // Converts the band index from 'source' space to 'destination' space.
                if (targetBands != null && bandIndex < targetBands.length) {
                    bandIndex = targetBands[bandIndex];
                }
                /*
                 * For floating point types, replaces no-data values by NaN because the floating
                 * point numbers are typically used for geophysics data, so the raster is likely
                 * to be a "geophysics" view for GridCoverage2D. All other values are stored "as
                 * is" without any offset.
                 *
                 * For integer types, if the range of values from the source data file fits into
                 * the range of values allowed by the destination raster, we will use an identity
                 * converter. If the only required conversion is a shift from negative to positive
                 * values, creates an offset converter with no-data values collapsed to 0.
                 */
                final SampleConverter converter;
                if (isFloat) {
                    converter = SampleConverter.createPadValuesMask(nodataValues);
                } else {
                    final boolean isZeroValid = (minimum <= 0 && maximum >= 0);
                    boolean collapsePadValues = false;
                    if (nodataValues != null && nodataValues.length != 0) {
                        final double[] sorted = (double[]) nodataValues.clone();
                        Arrays.sort(sorted);
                        double minFill = sorted[0];
                        double maxFill = minFill;
                        int indexMax = sorted.length;
                        while (--indexMax!=0 && Double.isNaN(maxFill = sorted[indexMax]));
                        assert minFill <= maxFill || Double.isNaN(minFill) : maxFill;
                        if (bandIndex == visibleBand && maxFill > maximumFillValue) {
                            maximumFillValue = maxFill;
                        }
                        if (minFill < floor || maxFill > ceil) {
                            // At least one fill value is outside the range of acceptable values.
                            collapsePadValues = true;
                        } else if (minimum >= 0) {
                            /*
                             * Arbitrary optimization of memory usage:  if there is a "large" empty
                             * space between the range of valid values and a no-data value, then we
                             * may (at subclass implementors choice) collapse the no-data values to
                             * zero in order to avoid wasting the empty space.  Note that we do not
                             * perform this collapse if the valid range contains negative values
                             * because it would not save any memory. We do not check the no-data
                             * values between 0 and 'minimum' for the same reason.
                             */
                            int k = Arrays.binarySearch(sorted, maximum);
                            if (k >= 0) k++; // We want the first element greater than maximum.
                            else k = ~k; // Really ~ operator, not -
                            if (k <= indexMax) {
                                double unusedSpace = Math.max(sorted[k] - maximum - 1, 0);
                                while (++k <= indexMax) {
                                    final double delta = sorted[k] - sorted[k-1] - 1;
                                    if (delta > 0) {
                                        unusedSpace += delta;
                                    }
                                }
                                final int unused = (int) Math.min(Math.round(unusedSpace), Integer.MAX_VALUE);
                                collapsePadValues = collapseNoDataValues(isZeroValid, sorted, unused);
                                // We invoked 'collapseNoDataValues' inconditionnaly even if
                                // 'unused' is zero because the user may decide on the basis
                                // of other criterions, like 'isZeroValid'.
                            }
                        }
                    }
                    if (minimum < floor || maximum > ceil) {
                        // The range of valid values is outside the range allowed by raw data type.
                        converter = SampleConverter.createOffset(1 - minimum, nodataValues);
                    } else if (collapsePadValues) {
                        if (isZeroValid) {
                            // We need to collapse the no-data values to 0, but it causes a clash
                            // with the range of valid values. So we also shift the later.
                            converter = SampleConverter.createOffset(1 - minimum, nodataValues);
                        } else {
                            // We need to collapse the no-data values and there is no clash.
                            converter = SampleConverter.createPadValuesMask(nodataValues);
                        }
                    } else {
                        /*
                         * Do NOT take 'nodataValues' in account if there is no need to collapse
                         * them. This is not the converter's job to transform "packed" values to
                         * "geophysics" values. We just want them to fit in the IndexColorModel,
                         * and they already fit. So the identity converter is appropriate even
                         * in presence of pad values.
                         */
                        converter = SampleConverter.IDENTITY;
                    }
                }
                if (converters!=null && bandIndex>=0 && bandIndex<converters.length) {
                    converters[bandIndex] = converter;
                }
                if (bandIndex == visibleBand) {
                    visibleConverter = converter;
                    visibleRange = range;
                }
            }
        }
        /*
         * Creates a color palette suitable for the range of values in the visible band.
         * The case for floating points is the simpliest: we should not have any offset,
         * at most a replacement of no-data values. In the case of integer values, we
         * must make sure that the indexed color map is large enough for containing both
         * the highest data value and the highest no-data value.
         */
        if (visibleRange == null) {
            visibleRange = (allRanges != null) ? allRanges : new NumberRange(floor, ceil);
        }
        final PaletteFactory factory = PaletteFactory.getDefault();
        factory.setWarningLocale(locale);
        final Palette palette;
        if (isFloat) {
            assert visibleConverter.getOffset() == 0 : visibleConverter;
            palette = factory.getContinuousPalette(paletteName, (float) visibleRange.getMinimum(),
                    (float) visibleRange.getMaximum(), dataType, numBands, visibleBand);
        } else {
            final double offset  = visibleConverter.getOffset();
            final double minimum = visibleRange.getMinimum();
            final double maximum = visibleRange.getMaximum();
            long lower, upper;
            if (minimum == Double.NEGATIVE_INFINITY) {
                lower = floor;
            } else {
                lower = Math.round(minimum + offset);
                if (!visibleRange.isMinIncluded()) {
                    lower++; // Must be inclusive
                }
            }
            if (maximum == Double.POSITIVE_INFINITY) {
                upper = ceil;
            } else {
                upper = Math.round(maximum + offset);
                if (visibleRange.isMaxIncluded()) {
                    upper++; // Must be exclusive
                }
            }
            final long size = Math.max(upper, Math.round(maximumFillValue) + 1);
            /*
             * The target lower, upper and size parameters are usually in the range of SHORT
             * or USHORT data type. The Palette class will performs the necessary checks and
             * throws an exception if those variables are out of range.  However, because we
             * need to cast to int before passing the parameter values,  we restrict them to
             * the 'int' range as a safety in order to avoid results that accidently fall in
             * the SHORT or USHORT range.  Because Integer.MIN_VALUE or MAX_VALUE are out of
             * range,  it doesn't matter if those values are inaccurate since we will get an
             * exception anyway.
             */
            palette = factory.getPalette(paletteName,
                    (int) Math.max(lower, Integer.MIN_VALUE),
                    (int) Math.min(upper, Integer.MAX_VALUE),
                    (int) Math.min(size,  Integer.MAX_VALUE), numBands, visibleBand);
        }
        return palette.getImageTypeSpecifier();
    }

    /**
     * Returns the data type which most closely represents the "raw" internal data of the image.
     * It should be one of {@link DataBuffer} constants. The default {@code GeographicImageReader}
     * implementation works better with the following types:
     *
     * {@link DataBuffer#TYPE_BYTE   TYPE_BYTE},
     * {@link DataBuffer#TYPE_SHORT  TYPE_SHORT},
     * {@link DataBuffer#TYPE_USHORT TYPE_USHORT} and
     * {@link DataBuffer#TYPE_FLOAT  TYPE_FLOAT}.
     *
     * The default implementation returns {@link DataBuffer#TYPE_FLOAT TYPE_FLOAT} in every cases.
     * <p>
     * <h3>Handling of negative integer values</h3>
     * If the raw internal data contains negative values but this method still declares a unsigned
     * integer type ({@link DataBuffer#TYPE_BYTE TYPE_BYTE} or {@link DataBuffer#TYPE_USHORT TYPE_USHORT}),
     * then the values will be translated in order to fit in the range of strictly positive values.
     * For example if the raw internal data range from -23000 to +23000, then there is a choice:
     *
     * <ul>
     *   <li><p>If this method returns {@link DataBuffer#TYPE_SHORT}, then the data will be
     *       stored "as is" without transformation. However the {@linkplain IndexColorModel
     *       index color model} will have the maximal length allowed by 16 bits integers, with
     *       positive values in the [0 .. {@value java.lang.Short#MAX_VALUE}] range and negative
     *       values wrapped in the [32768 .. 65535] range in two's complement binary form. The
     *       results is a color model consuming 256 kilobytes in every cases. The space not used
     *       by the [-23000 .. +23000] range (in the above example) is lost.</p></li>
     *
     *   <li><p>If this method returns {@link DataBuffer#TYPE_USHORT}, then the data will be
     *       translated to the smallest strictly positive range that can holds the data
     *       ([1..46000] for the above example). Value 0 is reserved for missing data. The
     *       result is a smaller {@linkplain IndexColorModel index color model} than the
     *       one used by untranslated data.</p></li>
     * </ul>
     *
     * @param  imageIndex The index of the image to be queried.
     * @return The data type ({@link DataBuffer#TYPE_FLOAT} by default).
     * @throws IOException If an error occurs reading the format information from the input source.
     *
     * @see #getRawImageType(int, ImageReadParam, SampleConverter[])
     */
    protected int getRawDataType(final int imageIndex) throws IOException {
        checkImageIndex(imageIndex);
        return DataBuffer.TYPE_FLOAT;
    }

    /**
     * Returns {@code true} if the no-data values should be collapsed to 0 in order to save memory.
     * This method is invoked automatically by the {@link #getRawImageType(int, ImageReadParam,
     * SampleConverter[]) getRawImageType} method when it detected some unused space between the
     * {@linkplain Band#getValidRange range of valid values} and at least one
     * {@linkplain Band#getNoDataValues no-data value}.
     * <p>
     * The default implementation returns {@code false} in all cases, thus avoiding arbitrary
     * choice. Subclasses can override this method with some arbitrary threashold, as in the
     * example below:
     *
     * <blockquote><pre>
     * return unusedSpace >= 1024;
     * </pre></blockquote>
     *
     * @param isZeroValid
     *          {@code true} if 0 is a valid value. If this method returns {@code true} while
     *          {@code isZeroValid} is {@code true}, then the {@linkplain SampleConverter sample
     *          converter} to be returned by {@link #getRawImageType(int, ImageReadParam,
     *          SampleConverter[]) getRawImageType} will offset all valid values by 1.
     * @param nodataValues
     *          The {@linkplain Arrays#sort(double[]) sorted}
     *          {@linkplain Band#getNoDataValues no-data values} (never null and never empty).
     * @param unusedSpace
     *          The largest amount of unused space outside the range of valid values.
     */
    protected boolean collapseNoDataValues(final boolean  isZeroValid,
                                           final double[] nodataValues,
                                           final int      unusedSpace)
    {
        return false;
    }

    /**
     * Returns the buffered image to which decoded pixel data should be written. The image
     * is determined by inspecting the supplied parameters if it is non-null, as described
     * in the {@linkplain #getDestination(ImageReadParam,Iterator,int,int) super-class method}.
     * In the default implementation, the {@linkplain ImageTypeSpecifier image type specifier}
     * set is a singleton containing only the {@linkplain #getRawImageType(int,ImageReadParam,
     * SampleConverter[]) raw image type}.
     * <p>
     * Implementations of the {@link #read(int,ImageReadParam)} method should invoke this
     * method instead of {@link #getDestination(ImageReadParam,Iterator,int,int)}.
     *
     * @param  imageIndex The index of the image to be retrieved.
     * @param  parameters The parameter given to the {@code read} method.
     * @param  width      The true width of the image or tile begin decoded.
     * @param  height     The true width of the image or tile being decoded.
     * @param  converters If non-null, an array where to store the converters required
     *                    for converting decoded pixel data into stored pixel data.
     * @return The buffered image to which decoded pixel data should be written.
     *
     * @throws IOException If an error occurs reading the format information from the input source.
     *
     * @see #getRawImageType(int, ImageReadParam, SampleConverter[])
     */
    protected BufferedImage getDestination(final int imageIndex, final ImageReadParam parameters,
                            final int width, final int height, final SampleConverter[] converters)
            throws IOException
    {
        final Set spi = Collections.singleton(getRawImageType(imageIndex, parameters, converters));
        return getDestination(parameters, spi.iterator(), width, height);
    }

    /**
     * Returns a default parameter object appropriate for this format. The default
     * implementation constructs and returns a new {@link GeographicImageReadParam}.
     *
     * @return An {@code ImageReadParam} object which may be used.
     *
     * @todo Replace the return type by {@link GeographicImageReadParam} when we will
     *       be allowed to compile for J2SE 1.5.
     */
    //@Override
    public ImageReadParam getDefaultReadParam() {
        return new GeographicImageReadParam(this);
    }

    /**
     * Reads the image indexed by {@code imageIndex} using a default {@link ImageReadParam}.
     * This is a convenience method that calls <code>{@linkplain #read(int,ImageReadParam)
     * read}(imageIndex, {@linkplain #getDefaultReadParam})</code>.
     * <p>
     * The default Java implementation passed a {@code null} parameter. This implementation
     * passes the default parameter instead in order to improve consistency when a subclass
     * overrides {@link #getDefaultReadParam}.
     *
     * @param imageIndex the index of the image to be retrieved.
     * @return the desired portion of the image.
     *
     * @throws IllegalStateException if the input source has not been set.
     * @throws IndexOutOfBoundsException if the supplied index is out of bounds.
     * @throws IOException if an error occurs during reading.
     */
    //@Override
    public BufferedImage read(final int imageIndex) throws IOException {
        return read(imageIndex, getDefaultReadParam());
    }

    /**
     * Flips the source region vertically. This method should be invoked straight after
     * {@link #computeRegions computeRegions} when the image to be read will be flipped
     * vertically, for example when the {@linkplain java.awt.image.Raster raster} sample
     * values are filled in a "{@code for (y=ymax-1; y>=ymin; y--)}" loop instead of
     * "{@code for (y=ymin; y<ymax; y++)}".
     * <p>
     * This method should be invoked as in the example below:
     *
     * <blockquote><pre>
     * computeRegions(param, srcWidth, srcHeight, image, srcRegion, destRegion);
     * flipVertically(param, srcHeight, srcRegion);
     * </pre></blockquote>
     *
     * @param param     The {@code param}     argument given to {@code computeRegions}.
     * @param srcHeight The {@code srcHeight} argument given to {@code computeRegions}.
     * @param srcRegion The {@code srcRegion} argument given to {@code computeRegions}.
     */
    protected static void flipVertically(final ImageReadParam param, final int srcHeight,
                                         final Rectangle srcRegion)
    {
        final int spaceLeft = srcRegion.y;
        srcRegion.y = srcHeight - (srcRegion.y + srcRegion.height);
        /*
         * After the flip performed by the above line, we still have 'spaceLeft' pixels left for
         * a downward translation.  We usually don't need to care about if, except if the source
         * region is very close to the bottom of the source image,  in which case the correction
         * computed below may be greater than the space left.
         *
         * We are done if there is no vertical subsampling. But if there is subsampling, then we
         * need an adjustment. The flipping performed above must be computed as if the source
         * region had exactly the size needed for reading nothing more than the last line, i.e.
         * 'srcRegion.height' must be a multiple of 'sourceYSubsampling' plus 1. The "offset"
         * correction is computed below accordingly.
         */
        if (param != null) {
            int offset = (srcRegion.height - 1) % param.getSourceYSubsampling();
            srcRegion.y += offset;
            offset -= spaceLeft;
            if (offset > 0) {
                // Happen only if we are very close to image border and
                // the above translation bring us outside the image area.
                srcRegion.height -= offset;
            }
        }
    }

    /**
     * Invoked when a warning occured. The default implementation make the following choice:
     * <p>
     * <ul>
     *   <li>If at least one {@linkplain IIOReadWarningListener warning listener}
     *       has been {@linkplain #addIIOReadWarningListener specified}, then the
     *       {@link IIOReadWarningListener#warningOccurred warningOccurred} method is
     *       invoked for each of them and the log record is <strong>not</strong> logged.</li>
     *
     *   <li>Otherwise, the log record is sent to the {@code "org.geotools.image.io"} logger.</li>
     * </ul>
     *
     * Subclasses may override this method if more processing is wanted, or for
     * throwing exception if some warnings should be considered as fatal errors.
     */
    public void warningOccurred(final LogRecord record) {
        if (warningListeners == null) {
            LOGGER.log(record);
        } else {
            processWarningOccurred(IndexedResourceBundle.format(record));
        }
    }

    /**
     * Convenience method for logging a warning from the given method.
     */
    private void warningOccurred(final String method, final String message) {
        final LogRecord record = new LogRecord(Level.WARNING, message);
        record.setSourceClassName(GeographicImageReader.class.getName());
        record.setSourceMethodName(method);
        warningOccurred(record);
    }

    /**
     * To be overriden and made {@code protected} by {@link StreamImageReader} only.
     */
    void close() throws IOException {
        metadata = null;
    }
}
