/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.sis.coverage;

import java.util.List;
import java.util.AbstractList;
import java.util.Arrays;
import java.util.Set;
import java.util.TreeSet;
import java.util.Collection;
import java.util.Collections;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.io.Serializable;
import javax.measure.Unit;
import org.opengis.util.GenericName;
import org.opengis.util.InternationalString;
import org.opengis.referencing.operation.MathTransform1D;
import org.apache.sis.referencing.operation.transform.TransferFunction;
import org.apache.sis.internal.util.UnmodifiableArrayList;
import org.apache.sis.internal.feature.Resources;
import org.apache.sis.measure.MeasurementRange;
import org.apache.sis.measure.NumberRange;
import org.apache.sis.util.resources.Vocabulary;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.iso.Names;
import org.apache.sis.util.Numbers;
import org.apache.sis.util.Debug;


/**
 * Describes the data values in a coverage (the range). For a raster, a sample dimension is a band.
 * A sample dimension can reserve some values for <cite>qualitative</cite> information like  “this
 * is a forest” and some other values for <cite>quantitative</cite> information like a temperature
 * measurements.
 *
 * <div class="note"><b>Example:</b>
 * an image of sea surface temperature (SST) could define the following categories:
 * <table class="sis">
 *   <caption>Example of categories in a sample dimension</caption>
 *   <tr><th>Values range</th> <th>Meaning</th></tr>
 *   <tr><td>[0]</td>          <td>No data</td></tr>
 *   <tr><td>[1]</td>          <td>Cloud</td></tr>
 *   <tr><td>[2]</td>          <td>Land</td></tr>
 *   <tr><td>[10…210]</td>     <td>Temperature to be converted into Celsius degrees through a linear equation</td></tr>
 * </table>
 * In this example, sample values in range [10…210] define a quantitative category, while all others categories are qualitative.
 * </div>
 *
 * <div class="section">Relationship with metadata</div>
 * This class provides the same information than ISO 19115 {@code org.opengis.metadata.content.SampleDimension},
 * but organized in a different way. The use of the same name may seem a risk, but those two types are typically
 * not used in same time.
 *
 * @author  Martin Desruisseaux (IRD, Geomatys)
 * @version 1.0
 * @since 1.0
 * @module
 */
public class SampleDimension implements Serializable {
    /**
     * Serial number for inter-operability with different versions.
     */
    private static final long serialVersionUID = -4966135180995819364L;

    /**
     * Identification for this sample dimension. Typically used as a way to perform a band select by
     * using human comprehensible descriptions instead of just numbers. Web Coverage Service (WCS)
     * can use this name in order to perform band sub-setting as directed from a user request.
     *
     * @see #getName()
     */
    private final GenericName name;

    /**
     * The background value, or {@code null} if unspecified. Should be a sample value of
     * a qualitative category in the {@link #categories} list, but this is not mandatory.
     *
     * @see #getBackground()
     */
    private final Number background;

    /**
     * The list of categories making this sample dimension. May be empty but shall never be null.
     */
    private final CategoryList categories;

    /**
     * The transform from samples to real values. May be {@code null} if this sample dimension
     * does not define any transform (which is not the same than defining an identity transform).
     *
     * @see #getTransferFunction()
     */
    private final MathTransform1D transferFunction;

    /**
     * The {@code SampleDimension} that describes values after {@linkplain #getTransferFunction() transfer function}
     * has been applied, or if this {@code SampleDimension} is already converted then the original sample dimension.
     * May be {@code null} if this sample dimension has no transfer function, or {@code this} if the transfer function
     * is the identity function.
     *
     * <p>This field establishes a bidirectional navigation between sample values and real values.
     * This is in contrast with methods named {@link #converted()}, which establish a unidirectional
     * navigation from sample values to real values.</p>
     *
     * @see #converted()
     * @see Category#converse
     * @see CategoryList#converse
     */
    private final SampleDimension converse;

    /**
     * Creates a new sample dimension for values that are already converted to real values.
     * This transfer function is set to identity, which implies that this constructor should
     * be invoked only for sample dimensions having at least one quantitative category.
     *
     * @param  original  the original sample dimension for packed values.
     * @param  bc        category of the background value in original sample dimension, or {@code null}.
     */
    private SampleDimension(final SampleDimension original, final Category bc) {
        converse         = original;
        name             = original.name;
        categories       = original.categories.converse;
        transferFunction = Category.identity();
        if (bc == null) {
            background = null;
        } else {
            background = bc.converse.range.getMinValue();
        }
    }

    /**
     * Creates a sample dimension with the specified name and categories.
     * The sample dimension name is used as a way to perform a band select
     * by using human comprehensible descriptions instead of numbers.
     * The background value is used for filling empty space in map reprojections.
     * The background value (if specified) should be the value of a qualitative category
     * present in the {@code categories} collection, but this is not mandatory.
     *
     * <p>Note that {@link Builder} provides a more convenient way to create sample dimensions.</p>
     *
     * @param name        an identification for the sample dimension.
     * @param background  the background value, or {@code null} if none.
     * @param categories  the list of categories. May be empty if none.
     */
    public SampleDimension(final GenericName name, final Number background, final Collection<? extends Category> categories) {
        ArgumentChecks.ensureNonNull("name", name);
        ArgumentChecks.ensureNonNull("categories", categories);
        final CategoryList list;
        if (categories.isEmpty()) {
            list = CategoryList.EMPTY;
        } else {
            list = CategoryList.create(categories.toArray(new Category[categories.size()]));
        }
        this.name       = name;
        this.background = background;
        this.categories = list;
        if (list.converse.range == null) {                  // Case where there is no quantitative category.
            transferFunction = null;
            converse = null;
        } else if (list == list.converse) {                 // Case where values are already converted.
            transferFunction = Category.identity();
            converse = this;
        } else {
            assert !list.isEmpty();                         // If empty, list.converse.range would have been null.
            transferFunction = list.getTransferFunction();
            converse = new SampleDimension(this, (background != null) ? list.search(background.doubleValue()) : null);
        }
    }

    /**
     * Returns the sample dimension that describes real values. This method establishes a unidirectional navigation
     * from sample values to real values. This is in contrast to {@link #converse}, which establish a bidirectional
     * navigation.
     *
     * @see #forConvertedValues(boolean)
     */
    private SampleDimension converted() {
        // Transfer function shall never be null if 'converse' is non-null.
        return (converse != null && !transferFunction.isIdentity()) ? converse : this;
    }

    /**
     * Returns an identification for this sample dimension. This is typically used as a way to perform a band select
     * by using human comprehensible descriptions instead of just numbers. Web Coverage Service (WCS) can use this name
     * in order to perform band sub-setting as directed from a user request.
     *
     * @return an identification of this sample dimension.
     *
     * @see org.opengis.metadata.content.RangeDimension#getSequenceIdentifier()
     */
    public GenericName getName() {
        return name;
    }

    /**
     * Returns all categories in this sample dimension. Note that a {@link Category} object may apply to an arbitrary range
     * of sample values. Consequently, the first element in this collection may not be directly related to the sample value
     * {@code 0}.
     *
     * @return the list of categories in this sample dimension, or an empty list if none.
     */
    @SuppressWarnings("ReturnOfCollectionOrArrayField")
    public List<Category> getCategories() {
        return categories;                      // Safe to return because immutable.
    }

    /**
     * Returns the background value. If this sample dimensions has quantitative categories, then the background
     * value should be one of the value returned by {@link #getNoDataValues()}. However this is not mandatory.
     *
     * @return the background value, typically (but not necessarily) one of {@link #getNoDataValues()}.
     */
    public Optional<Number> getBackground() {
        return Optional.ofNullable(background);
    }

    /**
     * Returns the values to indicate "no data" for this sample dimension.
     * If the sample dimension describes {@linkplain #forConvertedValues(boolean) converted values},
     * then the "no data values" are NaN values.
     *
     * @return the values to indicate no data values for this sample dimension, or an empty set if none.
     * @throws IllegalStateException if this method can not expand the range of no data values, for example
     *         because some ranges contain an infinite amount of values.
     */
    public Set<Number> getNoDataValues() {
        if (converse != null) {             // Null if SampleDimension does not contain at least one quantitative category.
            final boolean isConverted = transferFunction.isIdentity();
            final NumberRange<?>[] ranges = new NumberRange<?>[categories.size()];
            Class<? extends Number> widestClass = Byte.class;
            int count = 0;
            for (final Category category : categories) {
                final Category converted = category.converted();
                if ((isConverted || category != converted) && converted.isConvertedQualitative()) {
                    final NumberRange<?> range = category.range;
                    if (!range.isBounded()) {
                        throw new IllegalStateException(Resources.format(Resources.Keys.CanNotEnumerateValuesInRange_1, range));
                    }
                    widestClass = Numbers.widestClass(widestClass, range.getElementType());
                    ranges[count++] = range;
                }
            }
            if (count != 0) {
                final Set<Number> noDataValues = new TreeSet<>(SampleDimension::compare);
                for (int i=0; i<count; i++) {
                    final NumberRange<?> range = ranges[i];
                    final Number minimum = range.getMinValue();
                    final Number maximum = range.getMaxValue();
                    if (range.isMinIncluded()) noDataValues.add(Numbers.cast(minimum, widestClass));
                    if (range.isMaxIncluded()) noDataValues.add(Numbers.cast(maximum, widestClass));
                    if (Numbers.isInteger(range.getElementType())) {
                        long value = minimum.longValue() + 1;       // If value was inclusive, then it has already been added to the set.
                        long stop  = maximum.longValue() - 1;
                        while (value <= stop) {
                            noDataValues.add(Numbers.wrap(value, widestClass));
                        }
                    } else if (!minimum.equals(maximum)) {
                        throw new IllegalStateException(Resources.format(Resources.Keys.CanNotEnumerateValuesInRange_1, range));
                    }
                }
                return noDataValues;
            }
        }
        return Collections.emptySet();
    }

    /**
     * Compares as {@code double} values. This method is similar to {@link Double#compare(double,double)}
     * except that it also orders NaN values from raw bit patterns. Reminder: NaN values are sorted last.
     */
    private static int compare(final Number n1, final Number n2) {
        return Category.compare(n1.doubleValue(), n2.doubleValue());
    }

    /**
     * Returns the range of values occurring in this sample dimension. The range delimits sample values that
     * can be converted into real values using the {@linkplain #getTransferFunction() transfer function}.
     * If that function is {@linkplain MathTransform1D#isIdentity() identity}, then the values are already
     * real values and the range may be an instance of {@link MeasurementRange}
     * (i.e. a number range with units of measurement).
     *
     * @return the range of sample values in this sample dimension.
     */
    public Optional<NumberRange<?>> getSampleRange() {
        return Optional.ofNullable(categories.range);
    }

    /**
     * Returns the range of values after conversions by the transfer function.
     * This range is absent if there is no transfer function.
     *
     * @return the range of values after conversion by the transfer function.
     *
     * @see #getUnits()
     */
    public Optional<MeasurementRange<?>> getMeasurementRange() {
        if (converse == null) {
            return Optional.empty();
        }
        // A ClassCastException below would be a bug in our constructors.
        return Optional.ofNullable((MeasurementRange<?>) converted().categories.range);
    }

    /**
     * Returns the <cite>transfer function</cite> from sample values to real values.
     * This method returns a transform expecting sample values as input and computing real values as output.
     * The output units of measurement is given by {@link #getUnits()}.
     *
     * <p>This transform takes care of converting all "{@linkplain #getNoDataValues() no data values}" into {@code NaN} values.
     * The <code>transferFunction.{@linkplain MathTransform1D#inverse() inverse()}</code> transform is capable to differentiate
     * those {@code NaN} values and get back the original sample value.</p>
     *
     * @return the <cite>transfer function</cite> from sample to real values. May be absent if this sample dimension
     *         does not define any transform (which is not the same that defining an identity transform).
     */
    public Optional<MathTransform1D> getTransferFunction() {
        return Optional.ofNullable(transferFunction);
    }

    /**
     * Returns the scale factor and offset of the transfer function.
     * The formula returned by this method does <strong>not</strong> take
     * "{@linkplain #getNoDataValues() no data values}" in account.
     * For a more generic transfer function, see {@link #getTransferFunction()}.
     *
     * @return a description of the part of the transfer function working on real numbers.
     * @throws IllegalStateException if the transfer function can not be simplified in a form representable
     *         by {@link TransferFunction}.
     */
    public Optional<TransferFunction> getTransferFunctionFormula() {
        MathTransform1D tr = null;
        for (final Category category : categories) {
            final Optional<MathTransform1D> c = category.getTransferFunction();
            if (c.isPresent()) {
                if (tr == null) {
                    tr = c.get();
                } else if (!tr.equals(c.get())) {
                    throw new IllegalStateException(Resources.format(Resources.Keys.CanNotSimplifyTransferFunction_1));
                }
            }
        }
        if (tr == null) {
            return Optional.empty();
        }
        final TransferFunction f = new TransferFunction();
        try {
            f.setTransform(tr);
        } catch (IllegalArgumentException e) {
            throw new IllegalStateException(Resources.format(Resources.Keys.CanNotSimplifyTransferFunction_1, e));
        }
        return Optional.of(f);
    }

    /**
     * Returns the units of measurement for this sample dimension.
     * This unit applies to values obtained after the {@linkplain #getTransferFunction() transfer function}.
     * May be absent if not applicable.
     *
     * @return the units of measurement.
     * @throws IllegalStateException if this sample dimension use different units.
     *
     * @see #getMeasurementRange()
     */
    public Optional<Unit<?>> getUnits() {
        Unit<?> unit = null;
        for (final Category category : converted().categories) {
            final NumberRange<?> r = category.range;
            if (r instanceof MeasurementRange<?>) {
                final Unit<?> c = ((MeasurementRange<?>) r).unit();
                if (c != null) {
                    if (unit == null) {
                        unit = c;
                    } else if (!unit.equals(c)) {
                        throw new IllegalStateException();
                    }
                }
            }
        }
        return Optional.ofNullable(unit);
    }

    /**
     * Returns a sample dimension that describes real values or sample values, depending if {@code converted} is {@code true}
     * or {@code false} respectively.  If there is no {@linkplain #getTransferFunction() transfer function}, then this method
     * returns {@code this}.
     *
     * @param  converted  {@code true} for a sample dimension describing converted values,
     *                    or {@code false} for a sample dimension describing packed values.
     * @return a sample dimension describing converted or packed values, depending on {@code converted} argument value.
     *         May be {@code this} but never {@code null}.
     *
     * @see org.apache.sis.coverage.grid.GridCoverage#forConvertedValues(boolean)
     */
    public SampleDimension forConvertedValues(final boolean converted) {
        // Transfer function shall never be null if 'converse' is non-null.
        if (converse != null && transferFunction.isIdentity() != converted) {
            return converse;
        }
        return this;
    }

    /**
     * Returns a hash value for this sample dimension.
     */
    @Override
    public int hashCode() {
        return categories.hashCode() + 31*name.hashCode();
    }

    /**
     * Compares the specified object with this sample dimension for equality.
     *
     * @param  object  the object to compare with.
     * @return {@code true} if the given object is equals to this sample dimension.
     */
    @Override
    public boolean equals(final Object object) {
        if (object == this) {
            return true;
        }
        if (object instanceof SampleDimension) {
            final SampleDimension that = (SampleDimension) object;
            return name.equals(that.name) && Objects.equals(background, that.background) && categories.equals(that.categories);
        }
        return false;
    }

    /**
     * Returns a string representation of this sample dimension.
     * This string is for debugging purpose only and may change in future version.
     *
     * @return a string representation of this sample dimension for debugging purpose.
     */
    @Override
    public String toString() {
        return new SampleRangeFormat(Locale.getDefault()).write(new SampleDimension[] {this});
    }

    /**
     * Returns a string representation of the given sample dimensions.
     * This string is for debugging purpose only and may change in future version.
     *
     * @param  locale      the locale to use for formatting texts.
     * @param  dimensions  the sample dimensions to format.
     * @return a string representation of the given sample dimensions for debugging purpose.
     */
    @Debug
    public static String toString(final Locale locale, SampleDimension... dimensions) {
        ArgumentChecks.ensureNonNull("dimensions", dimensions);
        return new SampleRangeFormat(locale).write(dimensions);
    }




    /**
     * A mutable builder for creating an immutable {@link SampleDimension}.
     * The following properties can be set:
     *
     * <ul>
     *   <li>An optional name for the {@code SampleDimension}.</li>
     *   <li>A single optional category for the background value.</li>
     *   <li>An arbitrary amount of <cite>qualitative</cite> categories.</li>
     *   <li>An arbitrary amount of <cite>quantitative</cite> categories.</li>
     * </ul>
     *
     * A <cite>qualitative category</cite> is a range of sample values associated to a label (not numbers).
     * For example 0 = cloud, 1 = sea, 2 = land, <i>etc</i>.
     * A <cite>quantitative category</cite> is a range of sample values associated to numbers with units of measurement.
     * For example 10 = 1.0°C, 11 = 1.1°C, 12 = 1.2°C, <i>etc</i>.
     * Those three kinds of category are created by the following methods:
     *
     * <ul>
     *   <li>{@link #setBackground(CharSequence, Number)}</li>
     *   <li>{@link #addQualitative(CharSequence, NumberRange)}</li>
     *   <li>{@link #addQuantitative(CharSequence, NumberRange, MathTransform1D, Unit)}</li>
     * </ul>
     *
     * All other {@code addQualitative(…)} and {@code addQuantitative(…)} methods are convenience methods delegating
     * to above-cited methods. Qualitative and quantitative categories can be mixed in the same {@link SampleDimension},
     * provided that their ranges do not overlap.
     * After properties have been set, the sample dimension is created by invoking {@link #build()}.
     *
     * @author  Martin Desruisseaux (IRD, Geomatys)
     * @version 1.0
     * @since   1.0
     * @module
     */
    public static class Builder {
        /**
         * Identification for this sample dimension.
         */
        private GenericName dimensionName;

        /**
         * The categories for the sample dimension to create.
         * This list is modified by the following methods:
         *
         * <ul>
         *   <li>{@link #setBackground(CharSequence, Number)}</li>
         *   <li>{@link #addQualitative(CharSequence, NumberRange)}</li>
         *   <li>{@link #addQuantitative(CharSequence, NumberRange, MathTransform1D, Unit)}</li>
         *   <li>{@code categories().remove(int)}</li>
         *   <li>{@link #clear()}</li>
         * </ul>
         *
         * @see #categories()
         */
        private Category[] categories;

        /**
         * Number of valid elements in {@link #categories}.
         */
        private int count;

        /**
         * The ordinal NaN values used for this sample dimension.
         * The {@link Category} constructor uses this set for avoiding collisions.
         */
        private final ToNaN toNaN;

        /**
         * Creates an initially empty builder for a sample dimension.
         * Callers shall invoke at least one {@code addFoo(…)} method before {@link #build()}.
         */
        public Builder() {
            categories = new Category[10];
            toNaN      = new ToNaN();
        }

        /**
         * Sets an identification of the sample dimension.
         * This is the value to be returned by {@link SampleDimension#getName()}.
         * If this method is invoked more than once, then the last specified name prevails
         * (previous sample dimension names are discarded).
         *
         * @param  name  identification of the sample dimension.
         * @return {@code this}, for method call chaining.
         */
        public Builder setName(final GenericName name) {
            dimensionName = name;
            return this;
        }

        /**
         * Sets an identification of the sample dimension as a character sequence.
         * This is a convenience method for creating a {@link GenericName} from the given characters.
         *
         * @param  name  identification of the sample dimension.
         * @return {@code this}, for method call chaining.
         */
        public Builder setName(final CharSequence name) {
            dimensionName = createLocalName(name);
            return this;
        }

        /**
         * Sets an identification of the sample dimension as a band number.
         * This method should be used only when no more descriptive name is available.
         *
         * @param  band  sequence identifier of the sample dimension to create.
         * @return {@code this}, for method call chaining.
         */
        public Builder setName(final int band) {
            dimensionName = Names.createMemberName(null, null, band);
            return this;
        }

        /**
         * A common place where are created local names from character string.
         * For making easier to revisit if we want to add a namespace.
         */
        private static GenericName createLocalName(final CharSequence name) {
            return Names.createLocalName(null, null, name);
        }

        /**
         * Creates a range for the given minimum and maximum values. We use the static factory methods instead
         * than the {@link NumberRange} constructor for sharing existing range instances. This is also a way
         * to ensure that the number type is one of the primitive wrappers.
         *
         * <p>This method is invoked for qualitative categories only. For that reason, it accepts NaN values.</p>
         */
        private static NumberRange<?> range(final Class<?> type, Number minimum, Number maximum) {
            switch (Numbers.getEnumConstant(type)) {
                case Numbers.BYTE:    return NumberRange.create(minimum.byteValue(),  true, maximum.byteValue(),   true);
                case Numbers.SHORT:   return NumberRange.create(minimum.shortValue(), true, maximum.shortValue(),  true);
                case Numbers.INTEGER: return NumberRange.create(minimum.intValue(),   true, maximum.intValue(),    true);
                case Numbers.LONG:    return NumberRange.create(minimum.longValue(),  true, maximum.longValue(),   true);
                case Numbers.FLOAT: {
                    final float min = minimum.floatValue();
                    final float max = maximum.floatValue();
                    if (!Float.isNaN(min) || !Float.isNaN(max)) {       // Let 'create' throws an exception if only one value is NaN.
                        return NumberRange.create(min, true, max, true);
                    }
                    if (minimum.getClass() != Float.class) minimum = min;
                    if (maximum.getClass() != Float.class) maximum = max;
                    break;
                }
                default: {
                    final double min = minimum.doubleValue();
                    final double max = maximum.doubleValue();
                    if (!Double.isNaN(min) || !Double.isNaN(max)) {     // Let 'create' throws an exception if only one value is NaN.
                        return NumberRange.create(min, true, max, true);
                    }
                    if (minimum.getClass() != Double.class) minimum = min;
                    if (maximum.getClass() != Double.class) maximum = max;
                    break;
                }
            }
            @SuppressWarnings({"unchecked", "rawtypes"})
            final NumberRange<?> samples = new NumberRange(type, minimum, true, maximum, true);
            return samples;
        }

        /**
         * Adds a qualitative category and marks that category as the background value.
         * This is the value to be returned by {@link SampleDimension#getBackground()}.
         * If this method is invoked more than once, then the last specified value prevails
         * (previous values become ordinary qualitative categories).
         *
         * @param  name    the category name as a {@link String} or {@link InternationalString} object,
         *                 or {@code null} for a default "fill value" name.
         * @param  sample  the background value.
         * @return {@code this}, for method call chaining.
         */
        public Builder setBackground(CharSequence name, Number sample) {
            ArgumentChecks.ensureNonNull("sample", sample);
            if (name == null) {
                name = Vocabulary.formatInternational(Vocabulary.Keys.FillValue);
            }
            final NumberRange<?> samples = range(sample.getClass(), sample, sample);
            // Use of 'getMinValue()' below shall be consistent with ToNaN.remove(Category).
            toNaN.background = samples.getMinValue();
            add(new Category(name, samples, null, null, toNaN));
            return this;
        }

        /**
         * Adds a qualitative category for samples of the given boolean value.
         * The {@code true} value is represented by 1 and the {@code false} value is represented by 0.
         *
         * <div class="note"><b>Implementation note:</b>
         * this convenience method delegates to {@link #addQualitative(CharSequence, NumberRange)}.</div>
         *
         * @param  name    the category name as a {@link String} or {@link InternationalString} object,
         *                 or {@code null} for a default "no data" name.
         * @param  sample  the sample value as a boolean.
         * @return {@code this}, for method call chaining.
         */
        public Builder addQualitative(final CharSequence name, final boolean sample) {
            final byte value = sample ? (byte) 1 : 0;
            return addQualitative(name, NumberRange.create(value, true, value, true));
        }

        /**
         * Adds a qualitative category for samples of the given tiny (8 bits) integer value.
         * The argument is treated as a signed integer ({@value Byte#MIN_VALUE} to {@value Byte#MAX_VALUE}).
         *
         * <div class="note"><b>Implementation note:</b>
         * this convenience method delegates to {@link #addQualitative(CharSequence, NumberRange)}.</div>
         *
         * @param  name    the category name as a {@link String} or {@link InternationalString} object,
         *                 or {@code null} for a default "no data" name.
         * @param  sample  the sample value as an integer.
         * @return {@code this}, for method call chaining.
         */
        public Builder addQualitative(final CharSequence name, final byte sample) {
            return addQualitative(name, NumberRange.create(sample, true, sample, true));
        }

        /**
         * Adds a qualitative category for samples of the given short (16 bits) integer value.
         * The argument is treated as a signed integer ({@value Short#MIN_VALUE} to {@value Short#MAX_VALUE}).
         *
         * <div class="note"><b>Implementation note:</b>
         * this convenience method delegates to {@link #addQualitative(CharSequence, NumberRange)}.</div>
         *
         * @param  name    the category name as a {@link String} or {@link InternationalString} object,
         *                 or {@code null} for a default "no data" name.
         * @param  sample  the sample value as an integer.
         * @return {@code this}, for method call chaining.
         */
        public Builder addQualitative(final CharSequence name, final short sample) {
            return addQualitative(name, NumberRange.create(sample, true, sample, true));
        }

        /**
         * Adds a qualitative category for samples of the given integer value.
         * The argument is treated as a signed integer ({@value Integer#MIN_VALUE} to {@value Integer#MAX_VALUE}).
         *
         * <div class="note"><b>Implementation note:</b>
         * this convenience method delegates to {@link #addQualitative(CharSequence, NumberRange)}.</div>
         *
         * @param  name    the category name as a {@link String} or {@link InternationalString} object,
         *                 or {@code null} for a default "no data" name.
         * @param  sample  the sample value as an integer.
         * @return {@code this}, for method call chaining.
         */
        public Builder addQualitative(final CharSequence name, final int sample) {
            return addQualitative(name, NumberRange.create(sample, true, sample, true));
        }

        /**
         * Adds a qualitative category for samples of the given floating-point value.
         *
         * <div class="note"><b>Implementation note:</b>
         * this convenience method delegates to {@link #addQualitative(CharSequence, NumberRange)}.</div>
         *
         * @param  name    the category name as a {@link String} or {@link InternationalString} object,
         *                 or {@code null} for a default "no data" name.
         * @param  sample  the sample value as a real number.
         * @return {@code this}, for method call chaining.
         */
        public Builder addQualitative(final CharSequence name, final float sample) {
            return addQualitative(name, NumberRange.create(sample, true, sample, true));
        }

        /**
         * Adds a qualitative category for samples of the given double precision floating-point value.
         *
         * <div class="note"><b>Implementation note:</b>
         * this convenience method delegates to {@link #addQualitative(CharSequence, NumberRange)}.</div>
         *
         * @param  name    the category name as a {@link String} or {@link InternationalString} object,
         *                 or {@code null} for a default "no data" name.
         * @param  sample  the sample value as a real number.
         * @return {@code this}, for method call chaining.
         */
        public Builder addQualitative(final CharSequence name, final double sample) {
            return addQualitative(name, NumberRange.create(sample, true, sample, true));
        }

        /**
         * Adds a qualitative category for samples in the given range of values.
         *
         * <div class="note"><b>Implementation note:</b>
         * this convenience method delegates to {@link #addQualitative(CharSequence, NumberRange)}.</div>
         *
         * @param  name     the category name as a {@link String} or {@link InternationalString} object,
         *                  or {@code null} for a default "no data" name.
         * @param  minimum  the minimum sample value, inclusive.
         * @param  maximum  the maximum sample value, inclusive.
         * @return {@code this}, for method call chaining.
         * @throws IllegalArgumentException if the range is empty.
         */
        public Builder addQualitative(final CharSequence name, final Number minimum, final Number maximum) {
            return addQualitative(name, range(Numbers.widestClass(minimum, maximum), minimum, maximum));
        }

        /**
         * Adds a qualitative category for all samples in the specified range of values.
         * This is the most generic method for adding a qualitative category.
         * All other {@code addQualitative(name, …)} methods are convenience methods delegating their work to this method.
         *
         * @param  name     the category name as a {@link String} or {@link InternationalString} object,
         *                  or {@code null} for a default "no data" name.
         * @param  samples  the minimum and maximum sample values in the category.
         * @return {@code this}, for method call chaining.
         * @throws IllegalArgumentException if the given range is empty.
         */
        public Builder addQualitative(CharSequence name, final NumberRange<?> samples) {
            if (name == null) {
                name = Vocabulary.formatInternational(Vocabulary.Keys.Nodata);
            }
            add(new Category(name, samples, null, null, toNaN));
            return this;
        }

        /**
         * Constructs a quantitative category mapping samples to real values in the specified range.
         * Sample values in the {@code samples} range will be mapped to real values in the {@code converted} range
         * through a linear equation of the form:
         *
         * <blockquote><var>measure</var> = <var>sample</var> × <var>scale</var> + <var>offset</var></blockquote>
         *
         * where <var>scale</var> and <var>offset</var> coefficients are computed from the ranges supplied in arguments.
         * The units of measurement will be taken from the {@code converted} range if it is an instance of {@link MeasurementRange}.
         *
         * <p><b>Warning:</b> this method is provided for convenience when the scale and offset factors are not explicitly specified.
         * If those factor are available, then the other {@code addQuantitative(name, samples, …)} methods are more reliable.</p>
         *
         * <div class="note"><b>Implementation note:</b>
         * this convenience method delegates to {@link #addQuantitative(CharSequence, NumberRange, MathTransform1D, Unit)}.</div>
         *
         * @param  name        the category name as a {@link String} or {@link InternationalString} object.
         * @param  samples     the minimum and maximum sample values in the category. Element class is usually
         *                     {@link Integer}, but {@link Float} and {@link Double} values are accepted as well.
         * @param  converted   the range of real values for this category, as an instance of {@link MeasurementRange}
         *                     if those values are associated to an unit of measurement.
         * @return {@code this}, for method call chaining.
         * @throws ClassCastException if the range element class is not a {@link Number} subclass.
         * @throws IllegalArgumentException if the range is invalid.
         */
        public Builder addQuantitative(final CharSequence name, final NumberRange<?> samples, final NumberRange<?> converted) {
            ArgumentChecks.ensureNonNull("samples", samples);
            ArgumentChecks.ensureNonNull("converted", converted);
            /*
             * We need to perform calculation using the same "included versus excluded" characteristics for sample and converted
             * values. We pickup the characteristics of the range using floating point values because it is easier to adjust the
             * bounds of the range using integer values (we just add or subtract 1 for integers, while the amount to add to real
             * numbers is not so clear). If both ranges use floating point values, arbitrarily adjust the converted values.
             */
            final boolean isMinIncluded, isMaxIncluded;
            if (Numbers.isInteger(samples.getElementType())) {
                isMinIncluded = converted.isMinIncluded();                         // This is the usual case.
                isMaxIncluded = converted.isMaxIncluded();
            } else {
                isMinIncluded = samples.isMinIncluded();                            // Less common case.
                isMaxIncluded = samples.isMaxIncluded();
            }
            final double minValue  = converted.getMinDouble(isMinIncluded);
            final double Δvalue    = converted.getMaxDouble(isMaxIncluded) - minValue;
            final double minSample =   samples.getMinDouble(isMinIncluded);
            final double Δsample   =   samples.getMaxDouble(isMaxIncluded) - minSample;
            final double scale     = Δvalue / Δsample;
            final TransferFunction transferFunction = new TransferFunction();
            transferFunction.setScale(scale);
            transferFunction.setOffset(minValue - scale * minSample);               // TODO: use Math.fma with JDK9.
            return addQuantitative(name, samples, transferFunction.getTransform(),
                    (converted instanceof MeasurementRange<?>) ? ((MeasurementRange<?>) converted).unit() : null);
        }

        /**
         * Adds a quantitative category for values ranging from {@code minimum} to {@code maximum} inclusive
         * in the given units of measurement. The transfer function is set to identity.
         *
         * <div class="note"><b>Implementation note:</b>
         * this convenience method delegates to {@link #addQuantitative(CharSequence, NumberRange, MathTransform1D, Unit)}.</div>
         *
         * @param  name     the category name as a {@link String} or {@link InternationalString} object.
         * @param  minimum  the minimum value (inclusive) in the given units.
         * @param  maximum  the maximum value (inclusive) in the given units.
         * @param  units    the units of measurement.
         * @return {@code this}, for method call chaining.
         * @throws IllegalArgumentException if a value is NaN or if {@code minimum} is greater than {@code maximum}.
         */
        public Builder addQuantitative(CharSequence name, float minimum, float maximum, Unit<?> units) {
            return addQuantitative(name, MeasurementRange.create(minimum, true, maximum, true, units), Category.identity(), units);
        }

        /**
         * Adds a quantitative category for values ranging from {@code minimum} to {@code maximum} inclusive
         * in the given units of measurement. The transfer function is set to identity.
         *
         * <div class="note"><b>Implementation note:</b>
         * this convenience method delegates to {@link #addQuantitative(CharSequence, NumberRange, MathTransform1D, Unit)}.</div>
         *
         * @param  name     the category name as a {@link String} or {@link InternationalString} object.
         * @param  minimum  the minimum value (inclusive) in the given units.
         * @param  maximum  the maximum value (inclusive) in the given units.
         * @param  units    the units of measurement.
         * @return {@code this}, for method call chaining.
         * @throws IllegalArgumentException if a value is NaN or if {@code minimum} is greater than {@code maximum}.
         */
        public Builder addQuantitative(CharSequence name, double minimum, double maximum, Unit<?> units) {
            return addQuantitative(name, MeasurementRange.create(minimum, true, maximum, true, units), Category.identity(), units);
        }

        /**
         * Adds a quantitative category for sample values ranging from {@code lower} inclusive to {@code upper} exclusive.
         * Sample values are converted into real values using the following linear equation:
         *
         * <blockquote><var>measure</var> = <var>sample</var> × <var>scale</var> + <var>offset</var></blockquote>
         *
         * Results of above conversion are measurements in the units specified by the {@code units} argument.
         *
         * <div class="note"><b>Implementation note:</b>
         * this convenience method delegates to {@link #addQuantitative(CharSequence, NumberRange, MathTransform1D, Unit)}.</div>
         *
         * @param  name    the category name as a {@link String} or {@link InternationalString} object.
         * @param  lower   the lower sample value, inclusive.
         * @param  upper   the upper sample value, exclusive.
         * @param  scale   the scale value which is multiplied to sample values for the category. Must be different than zero.
         * @param  offset  the offset value to add to sample values for this category.
         * @param  units   the units of measurement of values after conversion by the scale factor and offset.
         * @return {@code this}, for method call chaining.
         * @throws IllegalArgumentException if {@code lower} is not smaller than {@code upper},
         *         or if {@code scale} or {@code offset} are not real numbers, or if {@code scale} is zero.
         */
        public Builder addQuantitative(CharSequence name, int lower, int upper, double scale, double offset, Unit<?> units) {
            final TransferFunction transferFunction = new TransferFunction();
            transferFunction.setScale(scale);
            transferFunction.setOffset(offset);
            return addQuantitative(name, NumberRange.create(lower, true, upper, false), transferFunction.getTransform(), units);
        }

        /**
         * Constructs a quantitative category for all samples in the specified range of values.
         * Sample values (usually integers) will be converted into real values
         * (usually floating-point numbers) through the {@code toUnits} transform.
         * Results of that conversion are measurements in the units specified by the {@code units} argument.
         *
         * <p>This is the most generic method for adding a quantitative category.
         * All other {@code addQuantitative(name, …)} methods are convenience methods delegating their work to this method.</p>
         *
         * @param  name     the category name as a {@link String} or {@link InternationalString} object.
         * @param  samples  the minimum and maximum sample values in the category. Element class is usually
         *                  {@link Integer}, but {@link Float} and {@link Double} types are accepted as well.
         * @param  toUnits  the transfer function from sample values to real values in the specified units.
         * @param  units    the units of measurement of values after conversion by the transfer function.
         * @return {@code this}, for method call chaining.
         * @throws ClassCastException if the range element class is not a {@link Number} subclass.
         * @throws IllegalArgumentException if the range is invalid.
         *
         * @see TransferFunction
         */
        public Builder addQuantitative(CharSequence name, NumberRange<?> samples, MathTransform1D toUnits, Unit<?> units) {
            ArgumentChecks.ensureNonNull("toUnits", toUnits);
            add(new Category(name, samples, toUnits, units, toNaN));
            return this;
        }

        /**
         * Adds the given category to the list. This method is not public because the category
         * should have been created with {@link #toNaN} passed in argument to its constructor.
         */
        private void add(final Category category) {
            if (count == categories.length) {
                categories = Arrays.copyOf(categories, count * 2);
            }
            categories[count++] = category;
        }

        /**
         * Returns the list of categories added so far. The returned list does not support the
         * {@link List#add(Object) add} operation, but supports the {@link List#remove(int) remove} operation.
         *
         * @return the current category list, read-only except for the {@code remove} operation.
         */
        public List<Category> categories() {
            return new AbstractList<Category>() {
                /** Returns the number of categories in this list. */
                @Override public int size() {
                    return count;
                }

                /** Returns the category at the given index. */
                @Override public Category get(int i) {
                    ArgumentChecks.ensureValidIndex(count, i);
                    return categories[i];
                }

                /** Removes the category at the given index. */
                @Override public Category remove(int i) {
                    ArgumentChecks.ensureValidIndex(count, i);
                    final Category c = categories[i];
                    System.arraycopy(categories, i+1, categories, i, --count - i);
                    categories[count] = null;
                    toNaN.remove(c);
                    return c;
                }
            };
        }

        /**
         * Creates a new sample with the properties defined to this builder.
         *
         * @return the sample dimension.
         */
        public SampleDimension build() {
            GenericName name = dimensionName;
defName:    if (name == null) {
                for (final Category category : categories) {
                    if (category.isQuantitative()) {
                        name = createLocalName(category.name);
                        break defName;
                    }
                }
                name = createLocalName(Vocabulary.formatInternational(Vocabulary.Keys.Untitled));
            }
            return new SampleDimension(name, toNaN.background, UnmodifiableArrayList.wrap(categories, 0, count));
        }

        /**
         * Reset this builder to the same state than after construction.
         * The sample dimension name, background values and all categories are discarded.
         * This method can be invoked when the same builder is reused for creating more than one sample dimension.
         */
        public void clear() {
            dimensionName = null;
            count = 0;
            Arrays.fill(categories, null);
            toNaN.clear();
        }
    }
}
