/*
 *    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.referencing.operation.transform;

// J2SE dependencies
import java.util.Arrays;

// OpenGIS dependencies
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.operation.Matrix;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransformFactory;

// Geotools dependencies
import org.geotools.factory.Hints;
import org.geotools.referencing.ReferencingFactoryFinder;
import org.geotools.referencing.operation.LinearTransform;
import org.geotools.referencing.operation.matrix.XMatrix;
import org.geotools.referencing.operation.matrix.MatrixFactory;
import org.geotools.referencing.operation.matrix.GeneralMatrix;
import org.geotools.resources.XArray;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.i18n.ErrorKeys;


/**
 * An utility class for the separation of {@linkplain ConcatenatedTransform concatenation} of
 * {@linkplain PassThroughTransform pass through transforms}. Given an arbitrary
 * {@linkplain MathTransform math transform}, this utility class will returns a new math transform
 * that operates only of a given set of source dimensions. For example if the supplied
 * {@code transform} has (<var>x</var>, <var>y</var>, <var>z</var>) inputs and
 * (<var>longitude</var>, <var>latitude</var>, <var>height</var>) outputs, then
 * the following code:
 *
 * <blockquote><pre>
 * {@linkplain #addSourceDimensionRange addSourceDimensionRange}(0, 2);
 * MathTransform mt = {@linkplain #separate separate}(transform);
 * </pre></blockquote>
 *
 * <P>will returns a transform with (<var>x</var>, <var>y</var>) inputs and (probably)
 * (<var>longitude</var>, <var>latitude</var>) outputs. The later can be verified with
 * a call to {@link #getTargetDimensions}.</P>
 *
 * @since 2.1
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/referencing/src/main/java/org/geotools/referencing/operation/transform/DimensionFilter.java $
 * @version $Id: DimensionFilter.java 29058 2008-02-03 17:47:07Z desruisseaux $
 * @author Martin Desruisseaux
 *
 * @todo This class is specific to Geotools implementation; it is better to avoid it if
 *       you can. It could be generalized a bit if we perform the same operations on
 *       {@link org.opengis.referencing.operation.CoordinateOperation} interfaces instead
 *       of math transforms. We should revisit this issue after grid coverage API has been
 *       revisited (since grid coverage is a user of this class).
 *
 * @todo This class contains a set of static methods that could be factored out in
 *       some kind of {@code org.geotools.util.SortedIntegerSet} implementation.
 */
public class DimensionFilter {
    /**
     * The input dimensions to keep.
     * This sequence can contains any integers in the range 0 inclusive to
     * <code>transform.{@linkplain MathTransform#getSourceDimensions getSourceDimensions()}</code>
     * exclusive.
     */
    private int[] sourceDimensions;

    /**
     * The output dimensions to keep.
     * This sequence can contains any integers in the range 0 inclusive to
     * <code>transform.{@linkplain MathTransform#getTargetDimensions getTargetDimensions()}</code>
     * exclusive.
     */
    private int[] targetDimensions;

    /**
     * The factory for the creation of new math transforms.
     */
    private final MathTransformFactory factory;

    /**
     * Constructs a dimension filter with the
     * {@linkplain ReferencingFactoryFinder#getMathTransformFactory default math transform factory}.
     */
    public DimensionFilter() {
        this(ReferencingFactoryFinder.getMathTransformFactory(null));
    }

    /**
     * Constructs a dimension filter with a
     * {@linkplain ReferencingFactoryFinder#getMathTransformFactory math transform factory
     * built using the provided hints}.
     *
     * @param hints Hints to control the creation of the {@link MathTransformFactory}.
     */
    public DimensionFilter(Hints hints) {
        this(ReferencingFactoryFinder.getMathTransformFactory(hints));
    }

    /**
     * Constructs a dimension filter with the specified factory.
     *
     * @param factory The factory for the creation of new math transforms.
     */
    public DimensionFilter(final MathTransformFactory factory) {
        this.factory = factory;
    }

    /**
     * Clear any {@linkplain #getSourceDimensions source} and
     * {@linkplain #getTargetDimensions target dimension} setting.
     */
    public void clear() {
        sourceDimensions = null;
        targetDimensions = null;
    }

    /**
     * Add an input dimension to keep. The {@code dimension} applies to the
     * source dimensions of the transform to be given to
     * <code>{@linkplain #separate separate}(transform)</code>.
     * The number must be in the range 0 inclusive to
     * <code>transform.{@linkplain MathTransform#getSourceDimensions getSourceDimensions()}</code>
     * exclusive.
     *
     * @param  dimension The dimension to add.
     * @throws IllegalArgumentException if {@code dimension} is negative.
     */
    public void addSourceDimension(final int dimension) throws IllegalArgumentException {
        sourceDimensions = add(sourceDimensions, dimension);
    }

    /**
     * Add input dimensions to keep. The {@code dimensions} apply to the
     * source dimensions of the transform to be given to
     * <code>{@linkplain #separate separate}(transform)</code>.
     * All numbers must be in the range 0 inclusive to
     * <code>transform.{@linkplain MathTransform#getSourceDimensions getSourceDimensions()}</code>
     * exclusive. The {@code dimensions} values must be in strictly increasing order.
     *
     * @param  dimensions The new sequence of dimensions.
     * @throws IllegalArgumentException if {@code dimensions} contains negative values or
     *         is not a strictly increasing sequence.
     */
    public void addSourceDimensions(final int[] dimensions) throws IllegalArgumentException {
        sourceDimensions = add(sourceDimensions, dimensions);
    }

    /**
     * Add a range of input dimensions to keep. The {@code lower} and {@code upper} values
     * apply to the source dimensions of the transform to be given to
     * <code>{@linkplain #separate separate}(transform)</code>.
     *
     * @param lower The lower dimension, inclusive. Must not be smaller than 0.
     * @param upper The upper dimension, exclusive. Must not be greater than
     * <code>transform.{@linkplain MathTransform#getSourceDimensions getSourceDimensions()}</code>.
     */
    public void addSourceDimensionRange(final int lower, final int upper)
            throws IllegalArgumentException
    {
        sourceDimensions = add(sourceDimensions, lower, upper);
    }

    /**
     * Returns the input dimensions. This information is available only if at least one
     * setter method has been explicitly invoked for source dimensions.
     *
     * @return The input dimension as a sequence of strictly increasing values.
     * @throws IllegalStateException if input dimensions have not been set.
     */
    public int[] getSourceDimensions() throws IllegalStateException {
        if (sourceDimensions != null) {
            return (int[]) sourceDimensions.clone();
        }
        throw new IllegalStateException();
    }

    /**
     * Add an output dimension to keep. The {@code dimension} applies to the
     * target dimensions of the transform to be given to
     * <code>{@linkplain #separate separate}(transform)</code>.
     * The number must be in the range 0 inclusive to
     * <code>transform.{@linkplain MathTransform#getTargetDimensions getTargetDimensions()}</code>
     * exclusive.
     *
     * @param  dimension The dimension to add.
     * @throws IllegalArgumentException if {@code dimension} is negative.
     */
    public void addTargetDimension(final int dimension) throws IllegalArgumentException {
        targetDimensions = add(targetDimensions, dimension);
    }

    /**
     * Add output dimensions to keep. The {@code dimensions} apply to the
     * target dimensions of the transform to be given to
     * <code>{@linkplain #separate separate}(transform)</code>.
     * All numbers must be in the range 0 inclusive to
     * <code>transform.{@linkplain MathTransform#getTargetDimensions getTargetDimensions()}</code>
     * exclusive. The {@code dimensions} values must be in strictly increasing order.
     *
     * @param  dimensions The new sequence of dimensions.
     * @throws IllegalArgumentException if {@code dimensions} contains negative values or
     *         is not a strictly increasing sequence.
     */
    public void addTargetDimensions(int[] dimensions) throws IllegalArgumentException {
        targetDimensions = add(targetDimensions, dimensions);
    }

    /**
     * Add a range of output dimensions to keep. The {@code lower} and {@code upper} values
     * apply to the target dimensions of the transform to be given to
     * <code>{@linkplain #separate separate}(transform)</code>.
     *
     * @param lower The lower dimension, inclusive. Must not be smaller than 0.
     * @param upper The upper dimension, exclusive. Must not be greater than
     * <code>transform.{@linkplain MathTransform#getTargetDimensions getTargetDimensions()}</code>.
     */
    public void addTargetDimensionRange(final int lower, final int upper)
            throws IllegalArgumentException
    {
        targetDimensions = add(targetDimensions, lower, upper);
    }

    /**
     * Returns the output dimensions. This information is available only if one of the following
     * conditions is meet:
     *
     * <ul>
     *   <li>Target dimensions has been explicitly set using setter methods.</li>
     *   <li>No target dimensions were set but <code>{@linkplain #separate separate}(transform)</code>
     *       has been invoked at least once, in which case the target dimensions are inferred
     *       automatically from the {@linkplain #getSourceDimensions source dimensions} and the
     *       {@code transform}.</li>
     * </ul>
     *
     * @return The output dimension as a sequence of strictly increasing values.
     * @throws IllegalStateException if this information is not available.
     */
    public int[] getTargetDimensions() throws IllegalStateException {
        if (targetDimensions != null) {
            return (int[]) targetDimensions.clone();
        }
        throw new IllegalStateException();
    }

    /**
     * Separates the specified math transform. This method returns a math transform that uses
     * only the specified {@linkplain #getSourceDimensions source dimensions} and returns only
     * the specified {@linkplain #getTargetDimensions target dimensions}. Special case:
     *
     * <ul>
     *   <li><p>If {@linkplain #getSourceDimensions source dimensions} are unspecified, then the
     *       returned transform will expects all source dimensions as input but will produces only
     *       the specified {@linkplain #getTargetDimensions target dimensions} as output.</p></li>
     *
     *   <li><p>If {@linkplain #getTargetDimensions target dimensions} are unspecified, then the
     *       returned transform will expects only the specified {@linkplain #getSourceDimensions
     *       source dimensions} as input, and the target dimensions will be inferred
     *       automatically.</p></li>
     * </ul>
     *
     * @param  transform The transform to separate.
     * @return The separated math transform.
     * @throws FactoryException if the transform can't be separated.
     */
    public MathTransform separate(MathTransform transform) throws FactoryException {
        if (sourceDimensions == null) {
            sourceDimensions = series(0, transform.getSourceDimensions());
            if (targetDimensions == null) {
                targetDimensions = series(0, transform.getTargetDimensions());
                return transform;
            }
            return separateOutput(transform);
        }
        final int[] target = targetDimensions;
        transform = separateInput(transform);
        assert XArray.isStrictlySorted(targetDimensions);
        if (target != null) {
            final int[] step = targetDimensions;
            targetDimensions = new int[target.length];
            for (int i=0; i<target.length; i++) {
                final int j = Arrays.binarySearch(step, target[i]);
                if (j < 0) {
                    /*
                     * The user is asking for some target dimensions that we can't keep, probably
                     * because at least one of the requested target dimension as a dependency to
                     * an source dimension that do not appears in the list of source dimensions to
                     * kept.
                     *
                     * TODO: provide a more accurate error message.
                     */
                    throw new FactoryException(Errors.format(ErrorKeys.INSEPARABLE_TRANSFORM));
                }
                targetDimensions[i] = j;
            }
            transform = separateOutput(transform);
            targetDimensions = target;
        }
        assert sourceDimensions.length == transform.getSourceDimensions() : transform;
        assert targetDimensions.length == transform.getTargetDimensions() : transform;
        return transform;
    }

    /**
     * Separates the math transform on the basis of {@linkplain #sourceDimensions input dimensions}.
     * The remaining {@linkplain #targetDimensions output dimensions} will be selected automatically
     * according the specified input dimensions.
     *
     * @param  transform The transform to reduces.
     * @return A transform expecting only the specified input dimensions.
     * @throws FactoryException if the transform is not separable.
     */
    private MathTransform separateInput(final MathTransform transform) throws FactoryException {
        final int dimSource = transform.getSourceDimensions();
        final int dimTarget = transform.getTargetDimensions();
        final int dimInput  = sourceDimensions.length;
        final int lower     = sourceDimensions[0];
        final int upper     = sourceDimensions[dimInput-1] + 1;
        assert XArray.isStrictlySorted(sourceDimensions);
        if (upper > dimSource) {
            throw new IllegalArgumentException(Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$2,
                                               "sourceDimensions", new Integer(upper-1)));
        }
        /*
         * Check for easiest cases: same transform, identity transform or concatenated transforms.
         */
        if (dimInput == dimSource) {
            assert lower==0 && upper==dimSource;
            targetDimensions = series(0, dimTarget);
            return transform;
        }
        if (transform.isIdentity()) {
            targetDimensions = sourceDimensions;
            return factory.createAffineTransform(MatrixFactory.create(dimInput+1));
        }
        if (transform instanceof ConcatenatedTransform) {
            final ConcatenatedTransform ctr = (ConcatenatedTransform) transform;
            final int[] original = sourceDimensions;
            final MathTransform step1, step2;
            step1 = separateInput(ctr.transform1); sourceDimensions = targetDimensions;
            step2 = separateInput(ctr.transform2); sourceDimensions = original;
            return factory.createConcatenatedTransform(step1, step2);
        }
        /*
         * Special case for the pass through transform:  if at least one input dimension
         * belong to the passthrough's sub-transform, then delegates part of the work to
         * {@code subTransform(passThrough.transform, ...)}
         */
        if (transform instanceof PassThroughTransform) {
            final PassThroughTransform passThrough = (PassThroughTransform) transform;
            final int dimPass  = passThrough.subTransform.getSourceDimensions();
            final int dimDiff  = passThrough.subTransform.getTargetDimensions() - dimPass;
            final int subLower = passThrough.firstAffectedOrdinate;
            final int subUpper = subLower + dimPass;
            final DimensionFilter subFilter = new DimensionFilter(factory);
            for (int i=0; i<sourceDimensions.length; i++) {
                int n = sourceDimensions[i];
                if (n>=subLower && n<subUpper) {
                    // Dimension n belong to the subtransform.
                    subFilter.addSourceDimension(n - subLower);
                } else {
                    // Dimension n belong to heading or trailing dimensions.
                    // Passthrough, after adjustement for trailing dimensions.
                    if (n >= subUpper) {
                        n += dimDiff;
                    }
                    targetDimensions = add(targetDimensions, n);
                }
            }
            if (subFilter.sourceDimensions == null) {
                /*
                 * No source dimensions belong to the sub-transform. The only remaining
                 * sources are heading and trailing dimensions. A passthrough transform
                 * without its sub-transform is an identity transform...
                 */
                return factory.createAffineTransform(MatrixFactory.create(dimInput+1));
            }
            /*
             * There is at least one dimension to separate in the sub-transform. Performs this
             * separation and gets the list of output dimensions. We need to offset the output
             * dimensions by the amount of leading dimensions once the separation is done, in
             * order to translate from the sub-transform's dimension numbering to the transform's
             * numbering.
             */
            final MathTransform subTransform = subFilter.separateInput(passThrough.subTransform);
            for (int i=0; i<subFilter.targetDimensions.length; i++) {
                subFilter.targetDimensions[i] += subLower;
            }
            targetDimensions = add(targetDimensions, subFilter.targetDimensions);
            /*
             * If all source dimensions not in the sub-transform are consecutive numbers, we can
             * use our pass though transform implementation. The "consecutive numbers" requirement
             * (expressed in the 'if' statement below) is a consequence of a limitation in our
             * current implementation: our pass through transform doesn't accept arbitrary index
             * for modified ordinates.
             */
            if (containsAll(sourceDimensions, lower, subLower) &&
                containsAll(sourceDimensions, subUpper, upper))
            {
                final int firstAffectedOrdinate = Math.max(0, subLower-lower);
                final int  numTrailingOrdinates = Math.max(0, upper-subUpper);
                return factory.createPassThroughTransform(
                        firstAffectedOrdinate, subTransform, numTrailingOrdinates);
            }
            // TODO: handle more general case here...
            targetDimensions = null; // Clear before to fallback on the LinearTransform case.
        }
        /*
         * If the transform is affine (or at least projective), express the transform as a matrix.
         * Then, select output dimensions that depends only on selected input dimensions. If an
         * output dimension depends on at least one discarted input dimension, then this output
         * dimension will be discarted as well.
         */
        if (transform instanceof LinearTransform) {
            int           nRows = 0;
            boolean  hasLastRow = false;
            final Matrix matrix = ((LinearTransform) transform).getMatrix();
            assert dimSource+1 == matrix.getNumCol() &&
                   dimTarget+1 == matrix.getNumRow() : matrix;
            double[][] rows = new double[dimTarget+1][];
reduce:     for (int j=0; j<rows.length; j++) {
                final double[] row = new double[dimInput+1];
                /*
                 * For each output dimension (i.e. a matrix row), find the matrix elements for
                 * each input dimension to be kept. If a dependance to at least one discarted
                 * input dimension is found, then the whole output dimension is discarted.
                 *
                 * NOTE: The following loop stops at matrix.getNumCol()-1 because we don't
                 *       want to check the translation term.
                 */
                int nCols=0, scan=0;
                for (int i=0; i<dimSource; i++) {
                    final double element = matrix.getElement(j,i);
                    if (scan<sourceDimensions.length && sourceDimensions[scan]==i) {
                        row[nCols++] = element;
                        scan++;
                    } else if (element != 0) {
                        // Output dimension 'j' depends on one of discarted input dimension 'i'.
                        // The whole row will be discarted.
                        continue reduce;
                    }
                }
                row[nCols++] = matrix.getElement(j, dimSource); // Copy the translation term.
                assert nCols == row.length : nCols;
                if (j == dimTarget) {
                    hasLastRow = true;
                } else {
                    targetDimensions = add(targetDimensions, j);
                }
                rows[nRows++] = row;
            }
            rows = (double[][]) XArray.resize(rows, nRows);
            if (hasLastRow) {
                return factory.createAffineTransform(new GeneralMatrix(rows));
            }
            // In an affine transform,  the last row is not supposed to have dependency
            // to any input dimension. But in this particuler case, our matrix has such
            // dependencies. TODO: is there anything we could do about that?
        }
        throw new FactoryException(Errors.format(ErrorKeys.INSEPARABLE_TRANSFORM));
    }

    /**
     * Creates a transform which retains only a subset of an other transform's outputs. The number
     * and nature of inputs stay unchanged. For example if the supplied {@code transform} has
     * (<var>longitude</var>, <var>latitude</var>, <var>height</var>) outputs, then a sub-transform
     * may be used to keep only the (<var>longitude</var>, <var>latitude</var>) part. In most cases,
     * the created sub-transform is non-invertible since it loose informations.
     * <br><br>
     * This transform may be see as a non-square matrix transform with less rows
     * than columns, concatenated with {@code transform}. However, invoking
     * {@code createFilterTransfom(...)} allows the optimization of some common cases.
     *
     * @param  transform The transform to reduces.
     * @return The {@code transform} keeping only the output dimensions.
     * @throws FactoryException if the transform can't be created.
     */
    private MathTransform separateOutput(MathTransform transform) throws FactoryException {
        final int dimSource = transform.getSourceDimensions();
        final int dimTarget = transform.getTargetDimensions();
        final int dimOutput = targetDimensions.length;
        final int lower     = targetDimensions[0];
        final int upper     = targetDimensions[dimOutput-1];
        assert XArray.isStrictlySorted(targetDimensions);
        if (upper > dimTarget) {
            throw new IllegalArgumentException(Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$2,
                                               "targetDimensions", new Integer(upper)));
        }
        if (dimOutput == dimTarget) {
            assert lower==0 && upper==dimTarget;
            return transform;
        }
        /*
         * If the transform is an instance of "pass through" transform but no dimension from its
         * subtransform is requested, then ignore the subtransform (i.e. treat the whole transform
         * as identity, except for the number of output dimension which may be different from the
         * number of input dimension).
         */
        int dimPass = 0;
        int dimDiff = 0;
        int dimStep = dimTarget;
        if (transform instanceof PassThroughTransform) {
            final PassThroughTransform passThrough = (PassThroughTransform) transform;
            final int subLower = passThrough.firstAffectedOrdinate;
            final int subUpper = subLower + passThrough.subTransform.getTargetDimensions();
            if (!containsAny(targetDimensions, subLower, subUpper)) {
                transform = null;
                dimStep = dimSource;
                dimPass = subLower;
                dimDiff = (subLower + passThrough.subTransform.getSourceDimensions()) - subUpper;
            }
        }
        /*
         * Creates the matrix to be used as a filter,       [x']     [1  0  0  0] [x]
         * and concatenates it to the transform. The        [z']  =  [0  0  1  0] [y]
         * matrix will contains only a 1 for the output     [1 ]     [0  0  0  1] [z]
         * dimension to keep, as in the following example:                        [1]
         */
        final XMatrix matrix = MatrixFactory.create(dimOutput+1, dimStep+1);
        matrix.setZero();
        for (int j=0; j<dimOutput; j++) {
            int i = targetDimensions[j];
            if (i >= dimPass) {
                i += dimDiff;
            }
            matrix.setElement(j, i, 1);
        }
        // Affine transform has one more row/column than dimension.
        matrix.setElement(dimOutput, dimStep, 1);
        MathTransform filtered = factory.createAffineTransform(matrix);
        if (transform != null) {
            filtered = factory.createConcatenatedTransform(transform, filtered);
        }
        return filtered;
    }

    /**
     * Returns {@code true} if the given sequence contains all index in the range {@code lower}
     * inclusive to {@code upper} exclusive.
     *
     * @param  sequence The {@link #sourceDimensions} or {@link #targetDimensions} sequence to test.
     * @param  lower The lower value, inclusive.
     * @param  upper The upper value, exclusive.
     * @return {@code true} if the full range was found in the sequence.
     */
    private static boolean containsAll(final int[] sequence, final int lower, int upper) {
        if (lower == upper) {
            return true;
        }
        if (sequence != null) {
            assert XArray.isStrictlySorted(sequence);
            int index = Arrays.binarySearch(sequence, lower);
            if (index >= 0) {
                index += --upper - lower;
                if (index>=0 && index<sequence.length) {
                    return sequence[index] == upper;
                }
            }
        }
        return false;
    }

    /**
     * Returns {@code true} if the given sequence contains any value in the given range.
     *
     * @param  sequence The {@link #sourceDimensions} or {@link #targetDimensions} sequence to test.
     * @param  lower The lower value, inclusive.
     * @param  upper The upper value, exclusive.
     * @return {@code true} if the sequence contains at least one value in the given range.
     */
    private static boolean containsAny(final int[] sequence, final int lower, final int upper) {
        if (upper == lower) {
            return true;
        }
        if (sequence != null) {
            assert XArray.isStrictlySorted(sequence);
            int index = Arrays.binarySearch(sequence, lower);
            if (index >= 0) {
                return true;
            }
            index = ~index; // Tild, not minus sign.
            return index < sequence.length  &&  sequence[index] < upper;
        }
        return false;
    }

    /**
     * Add the specified {@code dimension} to the specified sequence. Values are added
     * in increasing order. Duplicated values are not added.
     *
     * @param sequence The {@link #sourceDimensions} or {@link #targetDimensions} sequence to update.
     */
    private static int[] add(int[] sequence, int dimension) throws IllegalArgumentException {
        if (dimension < 0) {
            throw new IllegalArgumentException(Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$2,
                                               "dimension", new Integer(dimension)));
        }
        if (sequence == null) {
            return new int[] {dimension};
        }
        assert XArray.isStrictlySorted(sequence);
        int i = Arrays.binarySearch(sequence, dimension);
        if (i < 0) {
            i = ~i;   // Tild, not the minus sign.
            sequence = XArray.insert(sequence, i, 1);
            sequence[i] = dimension;
        }
        assert Arrays.binarySearch(sequence, dimension) == i;
        return sequence;
    }

    /**
     * Add the specified {@code dimensions} to the specified sequence. Values are added
     * in increasing order. Duplicated values are not added.
     *
     * @param sequence The {@link #sourceDimensions} or {@link #targetDimensions} sequence to update.
     */
    private static int[] add(int[] sequence, final int[] dimensions)
            throws IllegalArgumentException
    {
        if (dimensions.length != 0) {
            ensureValidSeries(dimensions);
            if (sequence == null) {
                sequence = (int[]) dimensions.clone();
            } else {
                // Note: the following loop is unefficient, but should suffise since this
                //       case should not occurs often and arrays should be small anyway.
                for (int i=0; i<dimensions.length; i++) {
                    sequence = add(sequence, dimensions[i]);
                }
            }
        }
        return sequence;
    }

    /**
     * Add the specified range to the specified sequence. Values are added
     * in increasing order. Duplicated values are not added.
     *
     * @param sequence The {@link #sourceDimensions} or {@link #targetDimensions} sequence to update.
     * @throws IllegalArgumentException if {@code lower} is not smaller than {@code upper}.
     */
    private static int[] add(int[] sequence, int lower, final int upper)
            throws IllegalArgumentException
    {
        if (lower<0 || lower>=upper) {
            throw new IllegalArgumentException(Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$2,
                                               "lower", new Integer(lower)));
        }
        if (sequence == null) {
            sequence = series(lower, upper);
        } else {
            // Note: the following loop is unefficient, but should suffise since this
            //       case should not occurs often and arrays should be small anyway.
            while (lower < upper) {
                sequence = add(sequence, lower++);
            }
        }
        assert containsAll(sequence, lower, upper);
        return sequence;
    }

    /**
     * Returns a series of increasing values starting at {@code lower}.
     */
    private static int[] series(final int lower, final int upper) throws IllegalArgumentException {
        final int[] sequence = new int[upper-lower];
        for (int i=0; i<sequence.length; i++) {
            sequence[i] = i+lower;
        }
        return sequence;
    }

    /**
     * Ensures that the specified array contains strictly increasing non-negative values.
     *
     * @param  dimensions The sequence to check.
     * @throws IllegalArgumentException if the specified sequence is not a valid series.
     */
    private static void ensureValidSeries(final int[] dimensions) throws IllegalArgumentException {
        int last = -1;
        for (int i=0; i<dimensions.length; i++) {
            final int value = dimensions[i];
            if (value <= last) {
                throw new IllegalArgumentException(Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$2,
                                                   "dimensions["+i+']', new Integer(value)));
            }
            last = value;
        }
    }
}
