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

import java.awt.image.DataBuffer;
import org.geotools.util.NumberRange;


/**
 * A {@code <SampleDimension>} element in
 * {@linkplain GeographicMetadataFormat geographic metadata format}.
 *
 * @since 2.4
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/unsupported/coverageio/src/main/java/org/geotools/image/io/metadata/Band.java $
 * @version $Id: Band.java 27583 2007-10-23 11:29:26Z desruisseaux $
 * @author Martin Desruisseaux
 *
 * @see org.opengis.coverage.SampleDimension
 */
public class Band extends MetadataAccessor {
    /**
     * Creates a parser for a band. This constructor should not be invoked
     * directly; use {@link GeographicMetadata#getBand} instead.
     *
     * @param metadata  The metadata which contains this band.
     * @param bandIndex The band index for this instance.
     */
    protected Band(final GeographicMetadata metadata, final int bandIndex) {
        this(metadata.getBands(), bandIndex);
    }

    /**
     * Creates a parser for a band. This constructor should not be invoked
     * directly; use {@link GeographicMetadata#getBand} instead.
     *
     * @param parent    The set of all bands.
     * @param bandIndex The band index for this instance.
     */
    Band(final ChildList parent, final int bandIndex) {
        super(parent);
        selectChild(bandIndex);
    }

    /**
     * Returns the name for this band, or {@code null} if none.
     */
    public String getName() {
        return getString("name");
    }

    /**
     * Sets the name for this band.
     *
     * @param name The band name, or {@code null} if none.
     */
    public void setName(final String name) {
        setString("name", name);
    }

    /**
     * Returns the range of valid values for this band. The range use the {@link Integer}
     * type if possible, or the {@link Double} type otherwise. Note that range
     * {@linkplain NumberRange#getMinValue minimum value},
     * {@linkplain NumberRange#getMaxValue maximum value} or both may be null if no
     * {@code "minValue"} or {@code "maxValue"} attribute were found for the
     * {@code "SampleDimensions/SampleDimension"} element.
     */
    public NumberRange getValidRange() {
        Number minimum, maximum;
        final boolean enabled = setWarningsEnabled(false);
        try {
            minimum = getInteger("minValue");
            maximum = getInteger("maxValue");
        } finally {
            setWarningsEnabled(enabled);
        }
        final Class type;
        if (minimum == null || maximum == null) {
            minimum = getDouble("minValue");
            maximum = getDouble("maxValue");
            type = Double.class;
        } else {
            type = Integer.class;
        }
        // Note: minimum and/or maximum may be null, in which case the range in unbounded.
        return new NumberRange(type, minimum, true, maximum, true);
    }

    /**
     * Sets the range of valid values. The values should be integers most of the time since
     * they are packed values (often index in a color palette). But floating point values
     * are allowed too.
     * <p>
     * If the minimal or maximal value may be unknown, consider invoking
     * <code>{@link #setPackedValues setPackedValues}(minValue, maxValue, &hellip;)</code>
     * instead. The later can infers default bounds according a given data type.
     *
     * @param minValue  The minimal valid <em>packed</em> value,
     *                  or {@link Double#NEGATIVE_INFINITY} if none.
     * @param maxValue  The maximal valid <em>packed</em> value,
     *                  or {@link Double#POSITIVE_INFINITY} if none.
     *
     * @see #setPackedValues
     */
    public void setValidRange(final double minValue, final double maxValue) {
        final int minIndex = (int) minValue;
        final int maxIndex = (int) maxValue;
        if (minIndex == minValue && maxIndex == maxValue) {
            setInteger("minValue", minIndex);
            setInteger("maxValue", maxIndex);
        } else {
            setDouble("minValue", minValue);
            setDouble("maxValue", maxValue);
        }
    }

    /**
     * Returns the fill values for this band, or {@code null} if none.
     */
    public double[] getNoDataValues() {
        return getDoubles("fillValues", true);
    }

    /**
     * Sets the fill values for this band. This method formats all fill values as integers
     * if possible, or all values as floating points otherwise. We apply a "all or nothing"
     * rule for consistency.
     *
     * @param fillValues The packed values used for missing data, or {@code null} if none.
     *
     * @see #setPackedValues
     */
    public void setNoDataValues(final double[] fillValues) {
        if (fillValues != null) {
            int[] asIntegers = new int[fillValues.length];
            for (int i=0; i<fillValues.length; i++) {
                final double value = fillValues[i];
                if ((asIntegers[i] = (int) value) != value) {
                    asIntegers = null; // Not integers; stop the check.
                    break;
                }
            }
            if (asIntegers != null) {
                setIntegers("fillValues", asIntegers);
                return;
            }
        }
        setDoubles("fillValues", fillValues);
    }

    /**
     * Defines valid and fill <em>packed</em> values as a combinaison of
     * <code>{@linkplain #setValidRange(double,double) setValidRange}(minValue, maxValue)</code>
     * and <code>{linkplain #setNoDataValues(double[]) setNoDataValues}(fillValues)</code>.
     * <p>
     * If the minimal or maximal value is {@linkplain Double#isInfinite infinite} and the data
     * type is an integer type, then this method replaces the infinite values by default bounds
     * inferred from the data type and the fill values.
     *
     * @param minValue   The minimal valid <em>packed</em> value,
     *                   or {@link Double#NEGATIVE_INFINITY} if unknown.
     * @param maxValue   The maximal valid <em>packed</em> value,
     *                   or {@link Double#POSITIVE_INFINITY} if unknown.
     * @param fillValues The packed values used for missing data, or {@code null} if none.
     * @param dataType   The raw data type as one of {@link DataBuffer} constants, or
     *                   {@link DataBuffer#TYPE_UNDEFINED} if unknown.
     *
     * @see #setValidRange
     * @see #setNoDataValues
     */
    public void setPackedValues(double minValue, double maxValue, final double[] fillValues,
            final int dataType)
    {
        minValue = replaceInfinity(minValue, fillValues, dataType);
        maxValue = replaceInfinity(maxValue, fillValues, dataType);
        setValidRange(minValue, maxValue);
        setNoDataValues(fillValues);
    }

    /**
     * If the specified value is infinity, then replace that values by a bounds inferred
     * from the specified fill values and data type.
     *
     * @param value      The value.
     * @param fillValues The packed values used for missing data, or {@code null} if none.
     * @param dataType   The raw data type as one of {@link DataBuffer} constants, or
     *                   {@link DataBuffer#TYPE_UNDEFINED} if unknown.
     */
    private static double replaceInfinity(double value, final double[] fillValues, final int dataType) {
        final boolean negative;
        if (value == Double.NEGATIVE_INFINITY) {
            negative = true;
        } else if (value == Double.POSITIVE_INFINITY) {
            negative = false;
        } else {
            return value;
        }
        final double midValue;
        switch (dataType) {
            default: {
                // Unsigned integer: computes the upper bound according the data length.
                final long range = 1L << DataBuffer.getDataTypeSize(dataType);
                value = negative ? 0 : range - 1;
                midValue = range >>> 1;
                break;
            }
            case DataBuffer.TYPE_SHORT: {
                value = negative ? Short.MIN_VALUE : Short.MAX_VALUE;
                midValue = 0;
                break;
            }
            case DataBuffer.TYPE_INT: {
                value = negative ? Integer.MIN_VALUE : Integer.MAX_VALUE;
                midValue = 0;
                break;
            }
            case DataBuffer.TYPE_FLOAT:
            case DataBuffer.TYPE_DOUBLE:
            case DataBuffer.TYPE_UNDEFINED: {
                // Unbounded or undefined type: nothing to do.
                return value;
            }
        }
        /*
         * Considers only the fill values that are close to the bounds we just computed. We use
         * the middle value (always 0 for signed data type) as the threshold for choosing which
         * bounds is close to that value. In other words, for signed data type we consider only
         * positive or negative fill values (depending the 'value' sign), not both in same time.
         *
         * For each fill value to consider, reduces the range of valid values in such a way that
         * it doesn't include that fill value. The exclusion is performed by substracting 1, which
         * should be okay since we known at this stage that the data type is integer.
         */
        if (fillValues != null) {
            double valueDistance = Math.abs(value - midValue);
            for (int i=0; i<fillValues.length; i++) {
                final double fillValue = fillValues[i];
                if ((fillValue < midValue) == negative) {
                    final double fillDistance = Math.abs(fillValue - midValue);
                    if (fillDistance <= valueDistance) {
                        valueDistance = fillDistance;
                        value         = fillValue - 1; // Value must be exclusive.
                    }
                }
            }
        }
        return value;
    }

    /**
     * Returns the scale factor from packed to geophysics values, or {@code 1} if none.
     */
    public double getScale() {
        final Double scale = getDouble("scale");
        return (scale != null) ? scale.doubleValue() : 1.0;
    }

    /**
     * Sets the scale factor for this band.
     *
     * @param scale The scale from packed to geophysics values, or {@code 1} if none.
     */
    public void setScale(final double scale) {
        setDouble("scale", scale);
    }

    /**
     * Returns the offset from packed to geophysics values, or {@code 0} if none.
     */
    public double getOffset() {
        final Double offset = getDouble("offset");
        return (offset != null) ? offset.doubleValue() : 0.0;
    }

    /**
     * Sets the offset for this band.
     *
     * @param offset The offset from packed to geophysics values, or {@code 0} if none.
     */
    public void setOffset(final double offset) {
        setDouble("offset", offset);
    }
}
