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

import java.util.List;
import java.util.Set;
import java.util.EnumSet;
import java.util.Objects;
import java.util.function.Function;
import java.lang.reflect.Field;
import java.lang.reflect.InaccessibleObjectException;
import java.awt.Shape;
import java.awt.Rectangle;
import java.awt.image.RenderedImage;
import javax.measure.Quantity;
import org.opengis.util.FactoryException;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransform1D;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.coverage.Category;
import org.apache.sis.coverage.RegionOfInterest;
import org.apache.sis.coverage.SampleDimension;
import org.apache.sis.coverage.SubspaceNotSpecifiedException;
import org.apache.sis.image.DataType;
import org.apache.sis.image.Colorizer;
import org.apache.sis.image.ImageProcessor;
import org.apache.sis.image.Interpolation;
import org.apache.sis.coverage.internal.SampleDimensions;
import org.apache.sis.coverage.internal.MultiSourceArgument;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.collection.WeakHashSet;
import org.apache.sis.util.internal.UnmodifiableArrayList;
import org.apache.sis.measure.NumberRange;


/**
 * A predefined set of operations on grid coverages.
 * After instantiation, {@code GridCoverageProcessor} can be configured for the following aspects:
 *
 * <ul class="verbose">
 *   <li>
 *     {@linkplain #setInterpolation(Interpolation) Interpolation method} to use during resampling operations.
 *   </li><li>
 *     {@linkplain #setFillValues(Number...) Fill values} to use for cells that cannot be computed.
 *   </li><li>
 *     {@linkplain #setColorizer(Colorizer) Colorization algorithm} to apply for colorizing a computed image.
 *   </li><li>
 *     {@linkplain #setPositionalAccuracyHints(Quantity...) Positional accuracy hints}
 *     for enabling the use of faster algorithm when a lower accuracy is acceptable.
 *   </li><li>
 *     {@linkplain #setOptimizations(Set) Optimizations} to enable.
 *   </li>
 * </ul>
 *
 * For each coverage operations, above properties are combined with parameters given to the operation method.
 *
 * <h2>Thread-safety</h2>
 * {@code GridCoverageProcessor} is safe for concurrent use in multi-threading environment.
 *
 * @author  Martin Desruisseaux (Geomatys)
 * @author  Alexis Manin (Geomatys)
 * @version 1.4
 *
 * @see org.apache.sis.image.ImageProcessor
 *
 * @since 1.1
 */
public class GridCoverageProcessor implements Cloneable {
    /**
     * Configured {@link ImageProcessor} instances used by {@link GridCoverage}s created by processors.
     * We use this set for sharing common instances in {@link GridCoverage} instances, which is okay
     * provided that we do not modify the {@link ImageProcessor} configuration.
     */
    private static final WeakHashSet<ImageProcessor> PROCESSORS = new WeakHashSet<>(ImageProcessor.class);

    /**
     * Returns a unique instance of the given processor. Both the given and the returned processors shall not
     * be modified after this method call, because they may be shared by many {@link GridCoverage} instances.
     * It implies that the given processor shall <em>not</em> be {@link #imageProcessor}. It must be a clone.
     *
     * @param  clone  a clone of {@link #imageProcessor} for which to return a unique instance.
     * @return a unique instance of the given clone. Shall not be modified by the caller.
     */
    static ImageProcessor unique(final ImageProcessor clone) {
        return PROCESSORS.unique(clone);
    }

    /**
     * Returns a unique instance of the current state of {@link #imageProcessor}.
     * Callers shall not modify the returned object because it may be shared by many {@link GridCoverage} instances.
     */
    private ImageProcessor snapshot() {
        ImageProcessor shared = PROCESSORS.get(imageProcessor);
        if (shared == null) {
            shared = unique(imageProcessor.clone());
        }
        return shared;
    }

    /**
     * The processor to use for operations on two-dimensional slices.
     */
    protected final ImageProcessor imageProcessor;

    /**
     * The set of optimizations that are enabled.
     * By default, this set contains all enumeration values.
     *
     * @see #getOptimizations()
     * @see #setOptimizations(Set)
     *
     * @since 1.3
     */
    protected final EnumSet<Optimization> optimizations = EnumSet.allOf(Optimization.class);

    /**
     * Creates a new processor with default configuration.
     */
    public GridCoverageProcessor() {
        imageProcessor = new ImageProcessor();
    }

    /**
     * Creates a new processor initialized to the given configuration.
     *
     * @param  processor  the processor to use for operations on two-dimensional slices.
     */
    public GridCoverageProcessor(final ImageProcessor processor) {
        ArgumentChecks.ensureNonNull("processor", processor);
        imageProcessor = processor.clone();
    }

    /**
     * Returns the interpolation method to use for resampling operations.
     * The default implementation delegates to the image processor.
     *
     * @return interpolation method to use in resampling operations.
     *
     * @see ImageProcessor#getInterpolation()
     */
    public Interpolation getInterpolation() {
        return imageProcessor.getInterpolation();
    }

    /**
     * Sets the interpolation method to use for resampling operations.
     * The default implementation delegates to the image processor.
     *
     * @param  method  interpolation method to use in resampling operations.
     *
     * @see ImageProcessor#setInterpolation(Interpolation)
     */
    public void setInterpolation(final Interpolation method) {
        imageProcessor.setInterpolation(method);
    }

    /**
     * Returns the values to use for pixels that cannot be computed.
     * The default implementation delegates to the image processor.
     *
     * @return fill values to use for pixels that cannot be computed, or {@code null} for the defaults.
     *
     * @see ImageProcessor#getFillValues()
     *
     * @since 1.2
     */
    public Number[] getFillValues() {
        return imageProcessor.getFillValues();
    }

    /**
     * Sets the values to use for pixels that cannot be computed.
     * The default implementation delegates to the image processor.
     *
     * @param  values  fill values to use for pixels that cannot be computed, or {@code null} for the defaults.
     *
     * @see ImageProcessor#setFillValues(Number...)
     *
     * @since 1.2
     */
    public void setFillValues(final Number... values) {
        imageProcessor.setFillValues(values);
    }

    /**
     * Returns the colorization algorithm to apply on computed images.
     * The default implementation delegates to the image processor.
     *
     * @return colorization algorithm to apply on computed image, or {@code null} for default.
     *
     * @see ImageProcessor#getColorizer()
     *
     * @since 1.4
     */
    public Colorizer getColorizer() {
        return imageProcessor.getColorizer();
    }

    /**
     * Sets the colorization algorithm to apply on computed images.
     * The colorizer is used by {@link #convert(GridCoverage, MathTransform1D[], Function) convert(…)}
     * and {@link #aggregateRanges(GridCoverage...) aggregateRanges(…)} operations among others.
     * The default implementation delegates to the image processor.
     *
     * @param colorizer colorization algorithm to apply on computed image, or {@code null} for default.
     *
     * @see ImageProcessor#setColorizer(Colorizer)
     * @see #visualize(GridCoverage, GridExtent)
     *
     * @since 1.4
     */
    public void setColorizer(final Colorizer colorizer) {
        imageProcessor.setColorizer(colorizer);
    }

    /**
     * Returns hints about the desired positional accuracy, in "real world" units or in pixel units.
     * The default implementation delegates to the image processor.
     *
     * @return desired accuracy in no particular order, or an empty array if none.
     *
     * @see ImageProcessor#getPositionalAccuracyHints()
     */
    public Quantity<?>[] getPositionalAccuracyHints() {
        return imageProcessor.getPositionalAccuracyHints();
    }

    /**
     * Sets hints about desired positional accuracy, in "real world" units or in pixel units.
     * The default implementation delegates to the image processor.
     *
     * @param  hints  desired accuracy in no particular order, or a {@code null} array if none.
     *                Null elements in the array are ignored.
     *
     * @see ImageProcessor#setPositionalAccuracyHints(Quantity...)
     */
    public void setPositionalAccuracyHints(final Quantity<?>... hints) {
        imageProcessor.setPositionalAccuracyHints(hints);
    }

    /**
     * Types of changes that a coverage processor can do for executing an operation more efficiently.
     * For example, the processor may, in some cases, replace an operation by a more efficient one.
     * Those optimizations should not change significantly the sample values at any given location,
     * but may change other aspects (in a compatible way) such as the {@link GridCoverage} subclass
     * returned or the size of the underlying rendered images.
     *
     * <p>By default the {@link #REPLACE_OPERATION} and {@link #REPLACE_SOURCE} optimizations are enabled.
     * Users may want to disable some optimizations for example in order to get more predictable results.</p>
     *
     * @author  Martin Desruisseaux (Geomatys)
     * @version 1.3
     *
     * @see #getOptimizations()
     * @see #setOptimizations(Set)
     *
     * @since 1.3
     */
    public enum Optimization {
        /**
         * Allows the replacement of an operation by a more efficient one.
         * This optimization is enabled by default.
         *
         * <h4>Example</h4>
         * If the {@link #resample(GridCoverage, GridGeometry) resample(…)} method is invoked with parameter values
         * that cause the resampling to be a translation of the grid by an integer amount of cells, then by default
         * {@link GridCoverageProcessor} will use the {@link #shiftGrid(GridCoverage, long[]) shiftGrid(…)}
         * algorithm instead. This option can be cleared for forcing a full resampling operation in all cases.
         */
        REPLACE_OPERATION,

        /**
         * Allows the replacement of source parameter by a more fundamental source.
         * This replacement may change the results, but usually with better accuracy.
         * This optimization is enabled by default.
         *
         * <h4>Example</h4>
         * If the {@link #resample(GridCoverage, GridGeometry) resample(…)} method is invoked with a source
         * grid coverage which is itself the result of a previous resampling, then instead of resampling an
         * already resampled coverage, by default {@link GridCoverageProcessor} will resample the original
         * coverage. This option can be cleared for disabling that replacement.
         */
        REPLACE_SOURCE
    }

    /**
     * Returns the set of optimizations that are enabled.
     * By default, the returned set contains all optimizations.
     *
     * <p>The returned set is a copy. Changes in this set will not affect the state of this processor.</p>
     *
     * @return copy of the set of optimizations that are enabled.
     * @since 1.3
     */
    public synchronized Set<Optimization> getOptimizations() {
        return optimizations.clone();
    }

    /**
     * Specifies the set of optimizations to enable.
     * All optimizations not in the given set will be disabled.
     *
     * @param enabled  set of optimizations to enable.
     * @since 1.3
     */
    public synchronized void setOptimizations(final Set<Optimization> enabled) {
        ArgumentChecks.ensureNonNull("enabled", enabled);
        optimizations.clear();
        optimizations.addAll(enabled);
    }

    /**
     * Applies a mask defined by a region of interest (ROI). If {@code maskInside} is {@code true},
     * then all pixels inside the given ROI are set to the {@linkplain #getFillValues() fill values}.
     * If {@code maskInside} is {@code false}, then the mask is reversed:
     * the pixels set to fill values are the ones outside the ROI.
     *
     * <h4>Properties used</h4>
     * This operation uses the following properties in addition to method parameters:
     * <ul>
     *   <li>{@linkplain #getFillValues() Fill values} values to assign to pixels inside/outside the region of interest.</li>
     * </ul>
     *
     * @param  source      the coverage on which to apply a mask.
     * @param  mask        region (in arbitrary CRS) of the mask.
     * @param  maskInside  {@code true} for masking pixels inside the shape, or {@code false} for masking outside.
     * @return a coverage with mask applied.
     * @throws TransformException if ROI coordinates cannot be transformed to grid coordinates.
     *
     * @see ImageProcessor#mask(RenderedImage, Shape, boolean)
     *
     * @since 1.2
     */
    public GridCoverage mask(final GridCoverage source, final RegionOfInterest mask, final boolean maskInside)
            throws TransformException
    {
        ArgumentChecks.ensureNonNull("source", source);
        ArgumentChecks.ensureNonNull("mask", mask);
        final Shape roi = mask.toShape2D(source.getGridGeometry());
        RenderedImage data = source.render(null);
        data = imageProcessor.mask(data, roi, maskInside);
        return new GridCoverage2D(source, data);
    }

    /**
     * Returns a coverage with sample values converted by the given functions.
     * The number of sample dimensions in the returned coverage is the length of the {@code converters} array,
     * which must be greater than 0 and not greater than the number of sample dimensions in the source coverage.
     * If the {@code converters} array length is less than the number of source sample dimensions,
     * then all sample dimensions at index ≥ {@code converters.length} will be ignored.
     *
     * <h4>Sample dimensions customization</h4>
     * By default, this method creates new sample dimensions with the same names and categories than in the
     * previous coverage, but with {@linkplain org.apache.sis.coverage.Category#getSampleRange() sample ranges}
     * converted using the given converters and with {@linkplain SampleDimension#getUnits() units of measurement}
     * omitted. This behavior can be modified by specifying a non-null {@code sampleDimensionModifier} function.
     * If non-null, that function will be invoked with, as input, a pre-configured sample dimension builder.
     * The {@code sampleDimensionModifier} function can {@linkplain SampleDimension.Builder#setName(CharSequence)
     * change the sample dimension name} or {@linkplain SampleDimension.Builder#categories() rebuild the categories}.
     *
     * <h4>Result relationship with source</h4>
     * If the source coverage is backed by a {@link java.awt.image.WritableRenderedImage},
     * then changes in the source coverage are reflected in the returned coverage and conversely.
     *
     * <h4>Properties used</h4>
     * This operation uses the following properties in addition to method parameters:
     * <ul>
     *   <li>{@linkplain #getColorizer() Colorizer} for customizing the rendered image color model.</li>
     * </ul>
     *
     * @param  source      the coverage for which to convert sample values.
     * @param  converters  the transfer functions to apply on each sample dimension of the source coverage.
     * @param  sampleDimensionModifier  a callback for modifying the {@link SampleDimension.Builder} default
     *         configuration for each sample dimension of the target coverage, or {@code null} if none.
     * @return the coverage which computes converted values from the given source.
     *
     * @see ImageProcessor#convert(RenderedImage, NumberRange<?>[], MathTransform1D[], DataType)
     *
     * @since 1.3
     */
    public GridCoverage convert(final GridCoverage source, MathTransform1D[] converters,
            Function<SampleDimension.Builder, SampleDimension> sampleDimensionModifier)
    {
        ArgumentChecks.ensureNonNull("source",     source);
        ArgumentChecks.ensureNonNull("converters", converters);
        final List<SampleDimension> sourceBands = source.getSampleDimensions();
        ArgumentChecks.ensureCountBetween("converters", true, 1, sourceBands.size(), converters.length);
        final SampleDimension[] targetBands = new SampleDimension[converters.length];
        final SampleDimension.Builder builder = new SampleDimension.Builder();
        if (sampleDimensionModifier == null) {
            sampleDimensionModifier = SampleDimension.Builder::build;
        }
        for (int i=0; i < converters.length; i++) {
            final MathTransform1D converter = converters[i];
            ArgumentChecks.ensureNonNullElement("converters", i, converter);
            final SampleDimension band = sourceBands.get(i);
            band.getBackground().ifPresent(builder::setBackground);
            band.getCategories().forEach((category) -> {
                if (category.isQuantitative()) {
                    // Unit is assumed different as a result of conversion.
                    builder.addQuantitative(category.getName(), category.getSampleRange(), converter, null);
                } else {
                    builder.addQualitative(category.getName(), category.getSampleRange());
                }
            });
            targetBands[i] = sampleDimensionModifier.apply(builder.setName(band.getName())).forConvertedValues(true);
            builder.clear();
        }
        return new ConvertedGridCoverage(source, UnmodifiableArrayList.wrap(targetBands),
                                         converters, true, snapshot(), true);
    }

    /**
     * Translates grid coordinates by the given amount of cells without changing "real world" coordinates.
     * The translated grid has the same {@linkplain GridExtent#getSize(int) size} than the source,
     * i.e. both low and high grid coordinates are displaced by the same amount of cells.
     * The "grid to CRS" transforms are adjusted accordingly in order to map to the same
     * "real world" coordinates.
     *
     * <h4>Number of arguments</h4>
     * The {@code translation} array length should be equal to the number of dimensions in the source coverage.
     * If the array is shorter, missing values default to 0 (i.e. no translation in unspecified dimensions).
     * If the array is longer, extraneous values are ignored.
     *
     * <h4>Optimizations</h4>
     * The following optimizations are applied by default and can be disabled if desired:
     * <ul>
     *   <li>{@link Optimization#REPLACE_SOURCE} for merging many calls
     *       of this {@code translate(…)} method into a single translation.</li>
     * </ul>
     *
     * <h4>Properties used</h4>
     * This operation uses the following properties in addition to method parameters:
     * <ul>
     *   <li>(none)</li>
     * </ul>
     *
     * @param  source       the grid coverage to translate.
     * @param  translation  translation to apply on each grid axis in order.
     * @return a grid coverage whose grid coordinates (both low and high ones) and
     *         the "grid to CRS" transforms have been translated by given amounts.
     *         If the given translation is a no-op (no value or only 0 ones), then the source is returned as is.
     * @throws ArithmeticException if the translation results in coordinates that overflow 64-bits integer.
     *
     * @see GridExtent#translate(long...)
     * @see GridGeometry#shiftGrid(long...)
     *
     * @since 1.3
     */
    public GridCoverage shiftGrid(final GridCoverage source, long... translation) {
        ArgumentChecks.ensureNonNull("source", source);
        ArgumentChecks.ensureNonNull("translation", translation);
        final boolean allowSourceReplacement;
        synchronized (this) {
            allowSourceReplacement = optimizations.contains(Optimization.REPLACE_SOURCE);
        }
        return TranslatedGridCoverage.create(source, null, translation, allowSourceReplacement);
    }

    /**
     * Creates a new coverage with a different grid extent, resolution or coordinate reference system.
     * The desired properties are specified by the {@link GridGeometry} argument, which may be incomplete.
     * The missing grid geometry components are completed as below:
     *
     * <table class="sis">
     *   <caption>Default values for undefined grid geometry components</caption>
     *   <tr>
     *     <th>Component</th>
     *     <th>Default value</th>
     *   </tr><tr>
     *     <td>{@linkplain GridGeometry#getExtent() Grid extent}</td>
     *     <td>A default size preserving resolution at source
     *       {@linkplain GridExtent#getPointOfInterest(PixelInCell) point of interest}.</td>
     *   </tr><tr>
     *     <td>{@linkplain GridGeometry#getGridToCRS Grid to CRS transform}</td>
     *     <td>Whatever it takes for fitting data inside the supplied extent.</td>
     *   </tr><tr>
     *     <td>{@linkplain GridGeometry#getCoordinateReferenceSystem() Coordinate reference system}</td>
     *     <td>Same as source coverage.</td>
     *   </tr>
     * </table>
     *
     * The interpolation method can be specified by {@link #setInterpolation(Interpolation)}.
     * If the grid coverage values are themselves interpolated, this method tries to use the
     * original data. The intent is to avoid adding interpolations on top of other interpolations.
     *
     * <h4>Optimizations</h4>
     * The following optimizations are applied by default and can be disabled if desired:
     * <ul>
     *   <li>{@link Optimization#REPLACE_SOURCE} for merging many calls of {@code resample(…)}
     *       or {@code translate(…)} method into a single resampling.</li>
     *   <li>{@link Optimization#REPLACE_OPERATION} for replacing {@code resample(…)} operation
     *       by {@code translate(…)} when possible.</li>
     * </ul>
     *
     * <h4>Properties used</h4>
     * This operation uses the following properties in addition to method parameters:
     * <ul>
     *   <li>{@linkplain #getInterpolation() Interpolation method} (nearest neighbor, bilinear, <i>etc</i>).</li>
     *   <li>{@linkplain #getFillValues() Fill values} for pixels outside source image.</li>
     *   <li>{@linkplain #getPositionalAccuracyHints() Positional accuracy hints}
     *       for enabling faster resampling at the cost of lower precision.</li>
     * </ul>
     *
     * @param  source  the grid coverage to resample.
     * @param  target  the desired geometry of returned grid coverage. May be incomplete.
     * @return a grid coverage with the characteristics specified in the given grid geometry.
     * @throws IncompleteGridGeometryException if the source grid geometry is missing an information.
     *         It may be the source CRS, the source extent, <i>etc.</i> depending on context.
     * @throws TransformException if some coordinates cannot be transformed to the specified target.
     *
     * @see ImageProcessor#resample(RenderedImage, Rectangle, MathTransform)
     */
    public GridCoverage resample(GridCoverage source, final GridGeometry target) throws TransformException {
        ArgumentChecks.ensureNonNull("source", source);
        ArgumentChecks.ensureNonNull("target", target);
        final boolean allowSourceReplacement, allowOperationReplacement;
        synchronized (this) {
            allowSourceReplacement    = optimizations.contains(Optimization.REPLACE_SOURCE);
            allowOperationReplacement = optimizations.contains(Optimization.REPLACE_OPERATION);
        }
        final boolean isConverted = source == source.forConvertedValues(true);
        /*
         * If the source coverage is already the result of a previous "resample" or "translate" operation,
         * use the original data in order to avoid interpolating values that are already interpolated.
         */
        for (;;) {
            if (ResampledGridCoverage.equivalent(source.getGridGeometry(), target)) {
                return source;
            } else if (allowSourceReplacement && source instanceof DerivedGridCoverage) {
                final DerivedGridCoverage derived = (DerivedGridCoverage) source;
                if (derived.isNotRepleacable()) break;
                source = derived.source;
            } else {
                break;
            }
        }
        /*
         * The projection are usually applied on floating-point values, in order
         * to gets maximal precision and to handle correctly the special case of
         * NaN values. However, we can apply the projection on integer values if
         * the interpolation type is "nearest neighbor" since this is not really
         * an interpolation.
         */
        if (!Interpolation.NEAREST.equals(imageProcessor.getInterpolation())) {
            source = source.forConvertedValues(true);
        }
        final GridCoverage resampled;
        try {
            // `ResampledGridCoverage` will create itself a clone of `imageProcessor`.
            resampled = ResampledGridCoverage.create(source, target, imageProcessor, allowOperationReplacement);
        } catch (IllegalGridGeometryException e) {
            final Throwable cause = e.getCause();
            if (cause instanceof TransformException) {
                throw (TransformException) cause;
            } else {
                throw e;
            }
        } catch (FactoryException e) {
            throw new TransformException(e.getMessage(), e);
        }
        return resampled.forConvertedValues(isConverted);
    }

    /**
     * Creates a new coverage with a different coordinate reference system.
     * The grid extent and "grid to CRS" transform are determined automatically
     * with default values preserving the resolution of source coverage at its
     * {@linkplain GridExtent#getPointOfInterest(PixelInCell) point of interest}.
     *
     * <p>See {@link #resample(GridCoverage, GridGeometry)} for more information
     * about interpolation and allowed optimizations.</p>
     *
     * @param  source  the grid coverage to resample.
     * @param  target  the desired coordinate reference system.
     * @return a grid coverage with the given coordinate reference system.
     * @throws IncompleteGridGeometryException if the source grid geometry is missing an information.
     * @throws TransformException if some coordinates cannot be transformed to the specified target.
     *
     * @since 1.3
     */
    public GridCoverage resample(final GridCoverage source, final CoordinateReferenceSystem target) throws TransformException {
        ArgumentChecks.ensureNonNull("source", source);
        ArgumentChecks.ensureNonNull("target", target);
        return resample(source, new GridGeometry(null, PixelInCell.CELL_CENTER, null, target));
    }

    /**
     * Automatically reduces a grid coverage dimensionality by removing all grid axes with an extent size of 1.
     * Axes in the reduced grid coverage will be in the same order than in the source coverage.
     *
     * @param  source  the coverage to reduce to a lower number of dimensions.
     * @return the reduced grid coverage, or {@code source} if no grid dimensions can be removed.
     *
     * @see DimensionalityReduction#reduce(GridGeometry)
     *
     * @since 1.4
     */
    public GridCoverage reduceDimensionality(final GridCoverage source) {
        ArgumentChecks.ensureNonNull("source", source);
        return DimensionalityReduction.reduce(source.getGridGeometry()).apply(source);
    }

    /**
     * Creates a coverage trimmed from the specified grid dimensions.
     * This is a <cite>dimensionality reduction</cite> operation applied to the coverage domain.
     * The dimensions to remove are specified as indices of <em>grid extent</em> axes.
     * It may be the same indices than the indices of the CRS axes which will be removed,
     * but not necessarily.
     *
     * <h4>Constraints</h4>
     * If the source coverage contains dimensions that are not
     * {@linkplain org.apache.sis.referencing.operation.transform.TransformSeparator separable}
     * and if only a subset of those dimensions are specified for removal,
     * then this method will throw an {@link IllegalGridGeometryException}.
     *
     * <p>For each dimension that is removed,
     * the {@linkplain GridExtent#getSize(int) size} of the grid extent must be 1 cell.
     * If this condition does not hold, then this method will throw a {@link SubspaceNotSpecifiedException}.
     * If desired, this restriction can be relaxed by direct use of {@link DimensionalityReduction} as below,
     * where (<var>x</var>, <var>y</var>, <var>z</var>, <var>t</var>) are grid coordinates of a point
     * in the desired slice:</p>
     *
     * {@snippet lang="java" :
     *     var reduction = DimensionalityReduction.remove(source.getGridGeometry(), gridAxesToPass);
     *     reduction = reduction.withSlicePoint(x, y, z, t);
     *     GridCoverage output = reduction.apply(source);
     *     }
     *
     * Alternatively the {@code withSlicePoint(…)} call can be omitted if the caller knows that the source
     * coverage can handle {@linkplain DimensionalityReduction#reverse(GridExtent) ambiguous grid extents}.
     *
     * @param  source            the coverage to reduce to a lower number of dimensions.
     * @param  gridAxesToRemove  indices of each grid dimension to strip from result. Duplicated values are ignored.
     * @return the reduced grid coverage, or {@code source} if no grid dimensions was specified.
     * @throws IndexOutOfBoundsException if a grid axis index is out of bounds.
     * @throws SubspaceNotSpecifiedException if at least one removed dimension has a grid extent size larger than 1 cell.
     * @throws IllegalGridGeometryException if the dimensions to keep cannot be separated from the dimensions to omit.
     *
     * @see DimensionalityReduction#remove(GridGeometry, int...)
     *
     * @since 1.4
     */
    public GridCoverage removeGridDimensions(final GridCoverage source, final int... gridAxesToRemove) {
        ArgumentChecks.ensureNonNull("source", source);
        var reduction = DimensionalityReduction.remove(source.getGridGeometry(), gridAxesToRemove);
        reduction.ensureIsSlice();
        return reduction.apply(source);
    }

    /**
     * Creates a coverage containing only the specified grid dimensions.
     * This is a <cite>dimensionality reduction</cite> operation applied to the coverage domain.
     * The dimensions to keep are specified as indices of <em>grid extent</em> axes.
     * It may be the same indices than the indices of the CRS axes which will pass through,
     * but not necessarily.
     *
     * <p>The axis order in the returned coverage is always the same as in the given {@code source} coverage,
     * whatever the order in which axes are specified as input in the {@code gridAxesToPass} array.
     * Duplicated values in the array are also ignored.</p>
     *
     * <h4>Constraints</h4>
     * If the source coverage contains dimensions that are not
     * {@linkplain org.apache.sis.referencing.operation.transform.TransformSeparator separable}
     * and if only a subset of those dimensions are selected in the {@code gridAxesToPass} array,
     * then this method will throw an {@link IllegalGridGeometryException}.
     *
     * <p>For each dimension that is not passed to the output grid coverage,
     * the {@linkplain GridExtent#getSize(int) size} of the grid extent must be 1 cell.
     * If this condition does not hold, then this method will throw a {@link SubspaceNotSpecifiedException}.
     * If desired, this restriction can be relaxed by direct use of {@link DimensionalityReduction} as below,
     * where (<var>x</var>, <var>y</var>, <var>z</var>, <var>t</var>) are grid coordinates of a point
     * in the desired slice:</p>
     *
     * {@snippet lang="java" :
     *     var reduction = DimensionalityReduction.select(source.getGridGeometry(), gridAxesToPass);
     *     reduction = reduction.withSlicePoint(x, y, z, t);
     *     GridCoverage output = reduction.apply(source);
     *     }
     *
     * Alternatively the {@code withSlicePoint(…)} call can be omitted if the caller knows that the source
     * coverage can handle {@linkplain DimensionalityReduction#reverse(GridExtent) ambiguous grid extents}.
     *
     * @param  source          the coverage to reduce to a lower number of dimensions.
     * @param  gridAxesToPass  indices of each grid dimension to maintain in result. Order and duplicated values are ignored.
     * @return the reduced grid coverage, or {@code source} if all grid dimensions where specified.
     * @throws IndexOutOfBoundsException if a grid axis index is out of bounds.
     * @throws SubspaceNotSpecifiedException if at least one removed dimension has a grid extent size larger than 1 cell.
     * @throws IllegalGridGeometryException if the dimensions to keep cannot be separated from the dimensions to omit.
     *
     * @see DimensionalityReduction#select(GridGeometry, int...)
     *
     * @since 1.4
     */
    public GridCoverage selectGridDimensions(final GridCoverage source, final int... gridAxesToPass) {
        ArgumentChecks.ensureNonNull("source", source);
        var reduction = DimensionalityReduction.select(source.getGridGeometry(), gridAxesToPass);
        reduction.ensureIsSlice();
        return reduction.apply(source);
    }

    /**
     * Selects a subset of sample dimensions (bands) in the given coverage.
     * This method can also be used for changing sample dimension order or
     * for repeating the same sample dimension from the source coverage.
     * If the specified {@code bands} indices select all sample dimensions
     * in the same order, then {@code source} is returned directly.
     *
     * @param  source  the coverage in which to select sample dimensions.
     * @param  bands   indices of sample dimensions to retain.
     * @return coverage width selected sample dimensions.
     * @throws IllegalArgumentException if a sample dimension index is invalid.
     *
     * @see ImageProcessor#selectBands(RenderedImage, int...)
     *
     * @since 1.4
     */
    public GridCoverage selectSampleDimensions(final GridCoverage source, final int... bands) {
        ArgumentChecks.ensureNonNull("source", source);
        return aggregateRanges(new GridCoverage[] {source}, new int[][] {bands});
    }

    /**
     * Aggregates in a single coverage the ranges of all specified coverages, in order.
     * The {@linkplain GridCoverage#getSampleDimensions() list of sample dimensions} of
     * the aggregated coverage will be the concatenation of the lists from all sources.
     *
     * <p>This convenience method delegates to {@link #aggregateRanges(GridCoverage[], int[][])}.
     * See that method for more information on restrictions.</p>
     *
     * @param  sources  coverages whose ranges shall be aggregated, in order. At least one coverage must be provided.
     * @return the aggregated coverage, or {@code sources[0]} returned directly if only one coverage was supplied.
     * @throws IllegalGridGeometryException if a grid geometry is not compatible with the others.
     *
     * @see #aggregateRanges(GridCoverage[], int[][])
     * @see ImageProcessor#aggregateBands(RenderedImage...)
     *
     * @since 1.4
     */
    public GridCoverage aggregateRanges(final GridCoverage... sources) {
        return aggregateRanges(sources, (int[][]) null);
    }

    /**
     * Aggregates in a single coverage the specified bands of a sequence of source coverages, in order.
     * This method performs the same work than {@link #aggregateRanges(GridCoverage...)},
     * but with the possibility to specify the sample dimensions to retain in each source coverage.
     * The {@code bandsPerSource} argument specifies the sample dimensions to keep, in order.
     * That array can be {@code null} for selecting all sample dimensions in all source coverages,
     * or may contain {@code null} elements for selecting all sample dimensions of the corresponding coverage.
     * An empty array element (i.e. zero sample dimension to select) discards the corresponding source coverage.
     *
     * <h4>Restrictions</h4>
     * <ul>
     *   <li>All coverage shall use the same CRS.</li>
     *   <li>All coverage shall use the same <cite>grid to CRS</cite> transform except for translation terms.</li>
     *   <li>Translation terms in <cite>grid to CRS</cite> can differ only by an integer amount of grid cells.</li>
     *   <li>The intersection of the domain of all coverages shall be non-empty.</li>
     *   <li>All coverages shall use the same data type in their rendered image.</li>
     * </ul>
     *
     * Some of those restrictions may be relaxed in future Apache SIS versions.
     *
     * @param  sources  coverages whose bands shall be aggregated, in order. At least one coverage must be provided.
     * @param  bandsPerSource  bands to use for each source coverage, in order. May contain {@code null} elements.
     * @return the aggregated coverage, or one of the sources if it can be used directly.
     * @throws IllegalGridGeometryException if a grid geometry is not compatible with the others.
     * @throws IllegalArgumentException if some band indices are duplicated or outside their range of validity.
     *
     * @see ImageProcessor#aggregateBands(RenderedImage[], int[][])
     *
     * @since 1.4
     */
    public GridCoverage aggregateRanges(GridCoverage[] sources, int[][] bandsPerSource) {
        final var aggregate = new MultiSourceArgument<>(sources, bandsPerSource);
        aggregate.unwrap(BandAggregateGridCoverage::unwrap);
        aggregate.completeAndValidate(GridCoverage::getSampleDimensions);
        aggregate.mergeConsecutiveSources();
        if (aggregate.isIdentity()) {
            return aggregate.sources()[0];
        }
        return new BandAggregateGridCoverage(aggregate, snapshot());
    }

    /**
     * Renders the given grid coverage as an image suitable for displaying purpose.
     * The resulting image is for visualization only and should not be used for computational purposes.
     * There is no guarantee about the number of bands in returned image or about which formula is used
     * for converting floating point values to integer values.
     *
     * <h4>How to specify colors</h4>
     * The image colors can be controlled by the {@link Colorizer} set on this coverage processor.
     * The recommended way is to associate colors to {@linkplain Category#getName() category names},
     * {@linkplain org.apache.sis.measure.MeasurementRange#unit() units of measurement}
     * or other category properties. Example:
     *
     * {@snippet lang="java" :
     *     Map<String,Color[]> colors = Map.of(
     *         "Temperature", new Color[] {Color.BLUE, Color.MAGENTA, Color.RED},
     *         "Wind speed",  new Color[] {Color.GREEN, Color.CYAN, Color.BLUE});
     *
     *     processor.setColorizer(Colorizer.forCategories((category) ->
     *         colors.get(category.getName().toString(Locale.ENGLISH))));
     *
     *     RenderedImage visualization = processor.visualize(source, slice);
     *     }
     *
     * <h4>Properties used</h4>
     * This operation uses the following properties in addition to method parameters:
     * <ul>
     *   <li>{@linkplain #getColorizer() Colorizer} for customizing the rendered image color model.</li>
     * </ul>
     *
     * @param  source  the grid coverage to visualize.
     * @param  slice   the slice and extent to render, or {@code null} for the whole coverage.
     * @return rendered image for visualization purposes only.
     * @throws IllegalArgumentException if the given extent does not have the same number of dimensions
     *         than the specified coverage or does not intersect.
     *
     * @see ImageProcessor#visualize(RenderedImage)
     *
     * @since 1.4
     */
    public RenderedImage visualize(final GridCoverage source, final GridExtent slice) {
        ArgumentChecks.ensureNonNull("source", source);
        final SampleDimension[] bands = source.getSampleDimensions().toArray(SampleDimension[]::new);
        final RenderedImage image = source.render(slice);
        try {
            SampleDimensions.IMAGE_PROCESSOR_ARGUMENT.set(bands);
            return imageProcessor.visualize(image);
        } finally {
            SampleDimensions.IMAGE_PROCESSOR_ARGUMENT.remove();
        }
    }

    /**
     * Invoked when an ignorable exception occurred.
     *
     * @param  caller  the method where the exception occurred.
     * @param  ex      the ignorable exception.
     */
    static void recoverableException(final String caller, final Exception ex) {
        Logging.recoverableException(GridExtent.LOGGER, GridCoverageProcessor.class, caller, ex);
    }

    /**
     * Returns {@code true} if the given object is a coverage processor
     * of the same class with the same configuration.
     *
     * @param  object  the other object to compare with this processor.
     * @return whether the other object is a coverage processor of the same class with the same configuration.
     */
    @Override
    public boolean equals(final Object object) {
        if (object != null && object.getClass() == getClass()) {
            final GridCoverageProcessor other = (GridCoverageProcessor) object;
            if (imageProcessor.equals(other.imageProcessor)) {
                final EnumSet<?> optimizations;
                synchronized (this) {
                    optimizations = (EnumSet<?>) this.optimizations.clone();
                }
                synchronized (other) {
                    return optimizations.equals(other.optimizations);
                }
            }
        }
        return false;
    }

    /**
     * Returns a hash code value for this coverage processor based on its current configuration.
     *
     * @return a hash code value for this processor.
     */
    @Override
    public synchronized int hashCode() {
        return Objects.hash(getClass(), imageProcessor, optimizations);
    }

    /**
     * Returns a coverage processor with the same configuration than this processor.
     *
     * @return a clone of this coverage processor.
     */
    @Override
    public GridCoverageProcessor clone() {
        try {
            final GridCoverageProcessor clone = (GridCoverageProcessor) super.clone();
            final Field f = GridCoverageProcessor.class.getDeclaredField("imageProcessor");
            f.setAccessible(true);      // Caller sensitive: must be invoked in same module.
            f.set(clone, imageProcessor.clone());
            return clone;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(e);
        } catch (ReflectiveOperationException e) {
            throw (InaccessibleObjectException) new InaccessibleObjectException().initCause(e);
        }
    }
}
