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

import java.awt.Dimension;
import java.awt.Transparency;
import java.awt.color.ColorSpace;
import java.awt.image.BandedSampleModel;
import java.awt.image.ColorModel;
import java.awt.image.ComponentSampleModel;
import java.awt.image.DataBuffer;
import java.awt.image.MultiPixelPackedSampleModel;
import java.awt.image.PixelInterleavedSampleModel;
import java.awt.image.SampleModel;
import java.awt.image.SinglePixelPackedSampleModel;

import javax.imageio.ImageReadParam;
import javax.imageio.ImageTypeSpecifier;
import javax.media.jai.ComponentSampleModelJAI;
import javax.media.jai.PlanarImage;

import org.geotools.resources.image.ComponentColorModelJAI;


/**
 * A class describing how a raw binary stream is to be decoded. In the context of
 * {@link RawBinaryImageReader}, the stream may not contains enough information
 * for an optimal decoding. For example the stream may not contains image's
 * width and height. The {@code RawBinaryImageReadParam} gives a chance
 * to specify those missing informations.
 *
 * @since 2.0
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/unsupported/coverageio/src/main/java/org/geotools/image/io/RawBinaryImageReadParam.java $
 * @version $Id: RawBinaryImageReadParam.java 26708 2007-08-27 19:42:59Z desruisseaux $
 * @author Martin Desruisseaux
 *
 * @todo We should consider to use Sun's RAW decoder provided with "Java Advanced Imaging Image I/O
 *       Tools" instead (download at http://java.sun.com/products/java-media/jai/). This replacement
 *       is not yet done because we need to figure out a way to handle pad values first.
 */
public class RawBinaryImageReadParam extends ImageReadParam {
    /**
     * The expected image model, or {@code null} if unknow.
     */
    private SampleModel model;
    
    /**
     * The expected image size, or {@code null} if unknow.
     */
    private Dimension size;
    
    /**
     * The expected data type, or {@link DataBuffer#TYPE_UNDEFINED} if unknow.
     */
    private int dataType = DataBuffer.TYPE_UNDEFINED;
    
    /**
     * The target data type, or {@link DataBuffer#TYPE_UNDEFINED} if not
     * defined. In the later case, the target data type will be the same
     * than the raw one.
     */
    private int targetDataType = DataBuffer.TYPE_UNDEFINED;
    
    /**
     * The pad value, or {@link Double#NaN} if there is none.
     */
    private double padValue = Double.NaN;
    
    /**
     * Constructs a new {@code RawBinaryImageReadParam} with default parameters.
     */
    public RawBinaryImageReadParam() {
    }
    
    /**
     * Specifies the image size in the input stream. Setting the size to {@code null}
     * reset the default size, which is reader dependent. Most readers will thrown an
     * exception at reading time if the image size is unspecified.
     *
     * @param size The expected image size, or {@code null} if unknow.
     */
    public void setStreamImageSize(final Dimension size) {
        this.size = (size!=null) ? new Dimension(size.width, size.height) : null;
    }
    
    /**
     * Returns the image size in the input stream, or {@code null} if unknow.
     * Image size is specified by the last call to {@link #setStreamImageSize} or
     * {@link #setStreamSampleModel}.
     */
    public Dimension getStreamImageSize() {
        return (size!=null) ? (Dimension) size.clone() : null;
    }
    
    /**
     * Checks the validity of the specified data type.
     *
     * @param  dataType The data type to check.
     * @throws IllegalArgumentException if {@code dataType} is not one of
     *         the valid enums of {@link DataBuffer}.
     */
    private static void checkDataType(final int dataType) throws IllegalArgumentException {
        if ((dataType < DataBuffer.TYPE_BYTE || dataType > DataBuffer.TYPE_DOUBLE) &&
             dataType != DataBuffer.TYPE_UNDEFINED)
        {
            throw new IllegalArgumentException(String.valueOf(dataType));
        }
    }
    
    /**
     * Specifies the data type in input stream. Setting data type to
     * {@link DataBuffer#TYPE_UNDEFINED} reset the default value, which
     * is reader dependent.
     *
     * @param dataType The data type, or {@link DataBuffer#TYPE_UNDEFINED} if unknow.
     *        Know data type should be a constant from {@link DataBuffer}. Common
     *        types are {@link DataBuffer#TYPE_INT}, {@link DataBuffer#TYPE_FLOAT}
     *        and {@link DataBuffer#TYPE_DOUBLE}.
     */
    public void setStreamDataType(final int dataType) {
        checkDataType(dataType);
        this.dataType = dataType;
    }
    
    /**
     * Returns the data type in input stream, or {@link DataBuffer#TYPE_UNDEFINED}
     * if unknow. Data type is specified by the last call to {@link #setStreamDataType}
     * or {@link #setStreamSampleModel}.
     */
    public int getStreamDataType() {
        return dataType;
    }
    
    /**
     * Sets the desired image type for the destination image, using one of
     * {@link DataBuffer} enumeration constant. This setting will override
     * any previous setting made with {@link #setDestinationType(ImageTypeSpecifier)}
     * or this {@code setDestinationType(int)} method.
     *
     * @param destType The data type. This should be a constant from {@link DataBuffer}.
     *        Common types are {@link DataBuffer#TYPE_INT}, {@link DataBuffer#TYPE_FLOAT}
     *        and {@link DataBuffer#TYPE_DOUBLE}.
     */
    public void setDestinationType(final int destType) {
        checkDataType(destType);
        targetDataType = destType;
        setDestinationType(getDestinationType(model!=null ? model.getNumBands() : 1));
    }
    
    /**
     * Creates a destination type with the specified number of bands.
     * If no such destination type is available, returns {@code null}.
     */
    final ImageTypeSpecifier getDestinationType(final int numBands) {
        if (targetDataType == DataBuffer.TYPE_UNDEFINED) {
            return null;
        }
        final SampleModel sampleModel;
        final ColorModel   colorModel;
        final ColorSpace   colorSpace = getColorSpace(numBands);
        if (model!=null) {
            /*
             * Case 1: we know the sample model for data in the
             *         underlying stream.  We will use the same
             *         model for the memory image, just changing
             *         the data type.
             */
            if (numBands != model.getNumBands()) {
                throw new IllegalArgumentException("Number of bands mismatch");
            }
            sampleModel = getStreamSampleModel(model, model, size, targetDataType);
        } else {
            /*
             * Case 2: We have to create a sample model from scratch.  We
             *         will use a banded sample model with some arbitrary
             *         color space  (which may be changed after the image
             *         reading is completed).
             */
            final int width, height;
            if (size!=null) {
                width  = size.width;
                height = size.height;
            } else {
                width = height = 1;
            }
            final int[] bankIndices = new int[numBands];
            final int[] bandOffsets = new int[numBands];
            for (int i=numBands; --i>=0;) bankIndices[i]=i;
            if (ContinuousPalette.USE_JAI_MODEL) {
                sampleModel = new ComponentSampleModelJAI(targetDataType, width, height,
                                                          1, width, bankIndices, bandOffsets);
            } else {
                return ImageTypeSpecifier.createBanded(colorSpace, bankIndices, bandOffsets,
                                                       targetDataType, false, false);
            }
        }
        /*
         * Constructs a color model likely to matches the
         * sample model, and then finish the type specifier.
         */
        if (sampleModel instanceof ComponentSampleModel) {
            // This is the most common case.
            colorModel = new ComponentColorModelJAI(getColorSpace(numBands),
                                                    false, false, Transparency.OPAQUE,
                                                    sampleModel.getDataType());
        } else {
            // Fallback to JAI helper method if we have a less common case.
            colorModel = PlanarImage.createColorModel(sampleModel);
        }
        return new ImageTypeSpecifier(colorModel, sampleModel);
    }
    
    /**
     * Returns a default color space for the destination sample model.
     * If no destination image has been specified, then a gray scale
     * color space will be constructed for values ranging from 0 to 1.
     */
    private ColorSpace getColorSpace(int numBands) {
        if (destination!=null) {
            return destination.getColorModel().getColorSpace();
        }
        /*
         * Overrides the number of source bands if this
         * parameter block contains enough informations.
         */
        if (sourceBands!=null) {
            numBands = sourceBands.length;
        } else if (model!=null) {
            numBands = model.getNumBands();
        }
        /*
         * Checks the number of destination bands. If 'destinationBands' is
         * null,  then all bands are going to be used.   If it is non-null,
         * then the destination image may have more bands than what we are
         * going to use. This problem still an open question... As a patch,
         * current implementation search for the greatest band number.
         */
        if (destinationBands!=null) {
            for (int i=0; i<destinationBands.length; i++) {
                if (destinationBands[i] >= numBands) {
                    numBands = destinationBands[i]+1;
                }
            }
        }
        if (numBands==1) {
            return ColorSpace.getInstance(ColorSpace.CS_GRAY);
        }
        return new ScaledColorSpace(numBands, 0, 0, 1);
    }
    
    /**
     * Set the pad value.
     *
     * @param padValue The pad value, or {@link Double#NaN} if there is none.
     */
    public void setPadValue(final double padValue) {
        this.padValue = padValue;
    }
    
    /**
     * Returns the pad value, or {@link Double#NaN} if there is none
     */
    public double getPadValue() {
        return padValue;
    }
    
    /**
     * Set a sample model indicating the data layout in the input stream.
     * Indications comprise image size and data type, i.e. calling this
     * method with a non-null value is equivalent to calling also the
     * following methods:
     *
     * <blockquote><pre>
     * setStreamImageSize(model.getWidth(), model.getHeight());
     * setStreamDataType(model.getDataType());
     * </pre></blockquote>
     *
     * Setting the sample model to {@code null} reset
     * the default model, which is reader dependent.
     */
    public void setStreamSampleModel(final SampleModel model) {
        this.model = model;
        if (model != null) {
            size = new Dimension(model.getWidth(), model.getHeight());
            dataType = model.getDataType();
        }
    }
    
    /**
     * Returns a sample model indicating the data layout in the input stream.
     * The {@link SampleModel}'s width and height should matches the image
     * size in the input stream.
     *
     * @return A sample model indicating the data layout in the input stream,
     *         or {@code null} if unknow.
     */
    public SampleModel getStreamSampleModel() {
        return model = getStreamSampleModel(null);
    }
    
    /**
     * Returns a sample model indicating the data layout in the input stream.
     * The {@link SampleModel}'s width and height should matches the image
     * size in the input stream.
     *
     * @param  defaultSampleModel A default sample model, or {@code null}
     *         if there is no default. If this {@code RawBinaryImageReadParam}
     *         contains unspecified sample model, image size or data type, values
     *         from {@code defaultSampleModel} will be used.
     * @return A sample model indicating the data layout in the input stream,
     *         or {@code null} if unknow.
     */
    final SampleModel getStreamSampleModel(final SampleModel defaultSampleModel) {
        return getStreamSampleModel(defaultSampleModel, model, size, dataType);
    }
    
    /**
     * Returns a sample model indicating the data layout in the input stream.
     * The {@link SampleModel}'s width and height should matches the image
     * size in the input stream.
     *
     * @param  defaultSampleModel A default sample model, or {@code null}
     *         if there is no default. If this {@code RawBinaryImageReadParam}
     *         contains unspecified sample model, image size or data type, values
     *         from {@code defaultSampleModel} will be used.
     * @param  model The sample model in the underlying stream, or {@code null}.
     * @param  size The image size in the underlying stream, or {@code null}.
     * @param  dataType the data type.
     * @return A sample model indicating the data layout in the input stream,
     *         or {@code null} if unknow.
     */
    private static SampleModel getStreamSampleModel(final SampleModel defaultSampleModel,
                                                    SampleModel model, Dimension size,
                                                    int dataType)
    {
        if (defaultSampleModel != null) {
            if (model == null) {
                model = defaultSampleModel;
            }
            if (size == null) {
                size = new Dimension(defaultSampleModel.getWidth(),
                                     defaultSampleModel.getHeight());
            }
            if (dataType == DataBuffer.TYPE_UNDEFINED) {
                dataType = defaultSampleModel.getDataType();
            }
        }
        if (model == null || size == null || dataType == DataBuffer.TYPE_UNDEFINED) {
            return null;
        }
        final int width  = size.width;
        final int height = size.height;
        if (dataType != model.getDataType()) {
            if (model instanceof ComponentSampleModel) {
                final ComponentSampleModel cast = (ComponentSampleModel) model;
                final int   pixelStride    = cast.getPixelStride();
                final int   scanlineStride = cast.getScanlineStride();
                final int[] bankIndices    = cast.getBankIndices();
                final int[] bandOffsets    = cast.getBandOffsets();
                if (model instanceof BandedSampleModel) {
                    model = new BandedSampleModel(dataType, width, height,
                                                  scanlineStride,
                                                  bankIndices, bandOffsets);
                } else if (model instanceof PixelInterleavedSampleModel) {
                    model = new PixelInterleavedSampleModel(dataType, width, height,
                                                            pixelStride, scanlineStride,
                                                            bandOffsets);
                } else if (model instanceof ComponentSampleModelJAI) {
                    model = new ComponentSampleModelJAI(dataType, width, height,
                                                        pixelStride, scanlineStride,
                                                        bankIndices, bandOffsets);
                } else {
                    model = new ComponentSampleModel(dataType, width, height,
                                                     pixelStride, scanlineStride,
                                                     bankIndices, bandOffsets);
                }
            } else if (model instanceof MultiPixelPackedSampleModel) {
                final MultiPixelPackedSampleModel cast = (MultiPixelPackedSampleModel) model;
                final int numberOfBits   = DataBuffer.getDataTypeSize(dataType);
                final int scanlineStride = cast.getScanlineStride();
                final int dataBitOffset  = cast.getDataBitOffset();
                model = new MultiPixelPackedSampleModel(dataType, width, height,
                                                        numberOfBits,
                                                        scanlineStride, dataBitOffset);
            } else if (model instanceof SinglePixelPackedSampleModel) {
                final SinglePixelPackedSampleModel cast = (SinglePixelPackedSampleModel) model;
                final int   scanlineStride = cast.getScanlineStride();
                final int[] bitMasks       = cast.getBitMasks();
                model = new SinglePixelPackedSampleModel(dataType, width, height,
                                                         scanlineStride, bitMasks);
            } else {
                throw new IllegalStateException(model.getClass().getName());
            }
        }
        if (model.getWidth() != width || model.getHeight() != height) {
            model = model.createCompatibleSampleModel(width, height);
        }
        return model;
    }
}
