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

import java.util.Map;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Objects;
import java.io.Serializable;
import org.opengis.geometry.Envelope;
import org.opengis.geometry.DirectPosition;
import org.opengis.geometry.MismatchedDimensionException;
import org.apache.sis.geometry.MismatchedReferenceSystemException;
import org.opengis.referencing.operation.Matrix;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.NoninvertibleTransformException;
import org.apache.sis.internal.referencing.DirectPositionView;
import org.apache.sis.internal.referencing.Resources;
import org.apache.sis.geometry.GeneralEnvelope;
import org.apache.sis.io.wkt.Formatter;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.ComparisonMode;
import org.apache.sis.util.Utilities;


/**
 * A transform having sub-areas where more accurate transforms can be used.
 * The global transform must be a reasonable approximation of the specialized transforms.
 * The lower and upper values of given envelopes are inclusive.
 *
 * @author  Martin Desruisseaux (Geomatys)
 * @version 1.0
 *
 * @see MathTransforms#specialize(MathTransform, Map)
 *
 * @since 1.0
 * @module
 */
class SpecializableTransform extends AbstractMathTransform implements Serializable {
    /**
     * For cross-version compatibility.
     */
    private static final long serialVersionUID = -7379277748632094312L;

    /**
     * The global transform to use if there is no suitable specialization.
     */
    private final MathTransform global;

    /**
     * The region where a transform is valid, together with the transform.
     * Contains also a chain of {@code SubArea}s fully included in this area.
     * Shall be unmodified after {@link SpecializableTransform} construction.
     */
    @SuppressWarnings("CloneableClassWithoutClone")                             // We will not use clone().
    private static final class SubArea extends GeneralEnvelope {
        /**
         * For cross-version compatibility.
         */
        private static final long serialVersionUID = 4197316795428796526L;

        /**
         * The transform to apply in this area.
         */
        final MathTransform transform;

        /**
         * The inverse of the transform, computed when first needed.
         * Synchronization for multi-threading is done (indirectly) in {@link SpecializableTransform#inverse()}.
         *
         * @see #createInverse(SubArea)
         */
        MathTransform inverse;

        /**
         * Specialization, or {@code null} if none. If non-null, that sub-area shall be fully included
         * in this {@code SubArea}. The specialization may itself contain another specialization. This
         * form a chain from this wider area to smallest area, where each step is a smaller area.
         *
         * <p>Note that this is note a substitute for an R-Tree. This is an optimization for the common
         * case where coordinates are close to each other (e.g. when iterating in a geometry), in which
         * case we can check immediately for inclusion in smaller areas before to check the wider area.</p>
         */
        private SubArea specialization;

        /**
         * Creates a new area where a transform is valid.
         */
        SubArea(final Envelope area, final MathTransform transform) {
            super(area);
            this.transform = transform;
        }

        /**
         * Tries to add a nested sub-area (a specialization of a specialization).
         *
         * @return whether the given area has been added.
         */
        boolean addSpecialization(final SubArea candidate) {
            if (specialization == null) {
                if (!contains(candidate)) {
                    return false;
                }
            } else if (!candidate.addSpecialization(specialization)) {
                return specialization.addSpecialization(candidate);
            }
            specialization = candidate;
            return true;
        }

        /**
         * Sets the CRS of all given ares to a common value. An exception is thrown if incompatible CRS are found.
         * This method does not verify the number of dimensions; this check should have been done by the caller.
         */
        static void uniformize(final SubArea[] domains) {
            CoordinateReferenceSystem common = null;
            for (SubArea area : domains) {
                do {
                    final CoordinateReferenceSystem crs = area.getCoordinateReferenceSystem();
                    if (common == null) {
                        common = crs;
                    } else if (crs != null && !Utilities.equalsIgnoreMetadata(common, crs)) {
                        throw new MismatchedReferenceSystemException(Errors.format(Errors.Keys.MismatchedCRS));
                    }
                } while ((area = area.specialization) != null);
            }
            for (SubArea area : domains) {
                do area.setCoordinateReferenceSystem(common);
                while ((area = area.specialization) != null);
            }
        }

        /**
         * Creates the inverse transforms. This method should be invoked only once when first needed
         * in a block synchronized (indirectly) by {@link SpecializableTransform#inverse()}.
         */
        static void createInverse(SubArea area) throws NoninvertibleTransformException {
            do {
                area.inverse = area.transform.inverse();
                area = area.specialization;
            } while (area != null);
        }

        /**
         * Returns the area that contains the given position, or {@code null} if none.
         * This method may be replaced by an R-Tree in a future Apache SIS version.
         */
        static SubArea find(final SubArea[] domains, final DirectPosition pos) {
            for (SubArea area : domains) {
                if (area.contains(pos)) {
                    SubArea next = area.specialization;
                    while (next != null && next.contains(pos)) {
                        area = next;
                        next = next.specialization;
                    }
                    return area;
                }
            }
            return null;
        }

        /**
         * Returns the area that contains the given position, looking only in the given area or its specializations.
         * Returns {@code null} if no area has been found.
         */
        static SubArea find(SubArea area, final DirectPosition pos) {
            SubArea found = null;
            while (area.contains(pos)) {
                found = area;
                area = area.specialization;
                if (area == null) break;
            }
            return found;
        }

        /**
         * Formats the given area and its transform as a pseudo-WKT.
         * For {@link SpecializableTransform#formatTo(Formatter)} implementation only.
         */
        static void format(SubArea area, final Formatter formatter) {
            while (area != null) {
                formatter.newLine(); formatter.append(area);
                formatter.newLine(); formatter.append(area.transform);
                area = area.specialization;
            }
        }

        /**
         * For {@link SpecializableTransform#computeHashCode()} implementation.
         */
        @Override
        public int hashCode() {
            int code = super.hashCode() ^ transform.hashCode();
            if (specialization != null) {
                code += 37 * specialization.hashCode();
            }
            return code;
        }

        /**
         * For {@link SpecializableTransform#equals(Object)} implementation.
         */
        @Override
        public boolean equals(final Object obj) {
            if (super.equals(obj)) {
                final SubArea other = (SubArea) obj;
                return transform.equals(other.transform) && Objects.equals(specialization, other.specialization);
            }
            return false;
        }

        /**
         * Formats this envelope as a "{@code DOMAIN}" element (non-standard).
         * This is used by {@link SpecializableTransform#formatTo(Formatter)}.
         */
        @Override
        protected String formatTo(final Formatter formatter) {
            super.formatTo(formatter);
            return "Domain";
        }
    }

    /**
     * Domains where specialized transforms are valid. The array should be very small.
     * In current implementation, elements in this array shall not overlap.
     * This array may be replaced by an R-Tree in a future Apache SIS version.
     */
    private final SubArea[] domains;

    /**
     * The inverse of this transform, computed when first needed.
     * Part of serialization for avoiding rounding error issues.
     *
     * @see #inverse()
     */
    private MathTransform inverse;

    /**
     * Creates a new transform with the given global transform and some amount of specializations.
     *
     * @param  global  the transform to use globally where there is no suitable specialization.
     * @param  specializations  more accurate transforms available in sub-areas.
     */
    SpecializableTransform(final MathTransform global, final Map<Envelope,MathTransform> specializations) {
        this.global = global;
        final int sourceDim = global.getSourceDimensions();
        final int targetDim = global.getTargetDimensions();
        final List<SubArea> areas = new ArrayList<>(specializations.size());
        for (final Map.Entry<Envelope,MathTransform> entry : specializations.entrySet()) {
            MathTransform tr = entry.getValue();
            ensureDimensionMatches(0, sourceDim, tr.getSourceDimensions());
            ensureDimensionMatches(1, targetDim, tr.getTargetDimensions());
            SubArea[] inherited = null;
            if (tr instanceof SpecializableTransform) {
                inherited = ((SpecializableTransform) tr).domains;
                tr = ((SpecializableTransform) tr).global;
            }
            final SubArea area = new SubArea(entry.getKey(), tr);
            addSpecialization(area, areas, sourceDim);
            /*
             * At this point we are usually done for the current SubArea. But if the given MathTransform
             * is another SpecializableTransform, then instead of storing nested SpecializableTransforms
             * we will store directly the specializations that it contains.  This will reduce the amount
             * of steps when transforming coordinates.
             */
            if (inherited != null) {
                for (final SubArea other : inherited) {
                    final SubArea e = new SubArea(other, other.transform);
                    e.intersect(area);
                    addSpecialization(e, areas, sourceDim);
                }
            }
        }
        domains = areas.toArray(new SubArea[areas.size()]);
        SubArea.uniformize(domains);
    }

    /**
     * Helper method for verifying transform dimension consistency.
     *
     * @param  type  0 if verifying source dimension, or 1 if verifying target dimension.
     */
    private static void ensureDimensionMatches(final int type, final int expected, final int actual) {
        if (expected != actual) {
            throw new MismatchedDimensionException(Resources.format(
                    Resources.Keys.MismatchedTransformDimension_3, type, expected, actual));
        }
    }

    /**
     * Verifies if the given {@code area} has the expected number of dimensions,
     * then adds it to {@code domains} list (eventually as a child of an existing node).
     *
     * @param  area     the new sub-area to add.
     * @param  domains  where to add the sub-area (not necessarily directly; maybe as a child of an existing node).
     * @param  dim      expected number of dimensions, for verification purpose.
     */
    private static void addSpecialization(final SubArea area, final List<SubArea> domains, final int dim) {
        if (!area.isEmpty()) {
            if (area.getDimension() != dim) {
                throw new MismatchedDimensionException(Errors.format(Errors.Keys.MismatchedDimension_3,
                            "envelope", dim, area.getDimension()));
            }
            for (final SubArea previous : domains) {
                if (previous.addSpecialization(area)) {
                    return;
                }
            }
            for (final SubArea previous : domains) {
                if (area.intersects(previous)) {
                    // Pending implementation of R-Tree in Apache SIS.
                    throw new IllegalArgumentException("Current implementation does not accept overlapping envelopes.");
                }
            }
            domains.add(area);
        }
    }

    /**
     * Gets the dimension of input points.
     */
    @Override
    public final int getSourceDimensions() {
        return global.getSourceDimensions();
    }

    /**
     * Gets the dimension of output points.
     */
    @Override
    public final int getTargetDimensions() {
        return global.getTargetDimensions();
    }

    /**
     * Returns the transform to use for the given domain.
     */
    private MathTransform forDomain(final SubArea domain) {
        return (domain != null) ? domain.transform : global;
    }

    /**
     * Transforms the specified {@code ptSrc} and stores the result in {@code ptDst}.
     * This method delegates to the most specialized transform.
     */
    @Override
    public final DirectPosition transform(final DirectPosition ptSrc, DirectPosition ptDst) throws TransformException {
        return forDomain(SubArea.find(domains, ptSrc)).transform(ptSrc, ptDst);
    }

    /**
     * Gets the derivative of this transform at a point.
     * This method delegates to the most specialized transform.
     */
    @Override
    public final Matrix derivative(final DirectPosition point) throws TransformException {
        return forDomain(SubArea.find(domains, point)).derivative(point);
    }

    /**
     * Transforms a single coordinate point in an array, and optionally computes the transform
     * derivative at that location. This method delegates to the most specialized transform.
     */
    @Override
    public final Matrix transform(final double[] srcPts, final int srcOff,
                                  final double[] dstPts, final int dstOff,
                                  boolean derivate) throws TransformException
    {
        final DirectPositionView pos = new DirectPositionView.Double(srcPts, srcOff, global.getSourceDimensions());
        final MathTransform tr = forDomain(SubArea.find(domains, pos));
        if (tr instanceof AbstractMathTransform) {
            return ((AbstractMathTransform) tr).transform(srcPts, srcOff, dstPts, dstOff, derivate);
        } else {
            Matrix derivative = derivate ? tr.derivative(pos) : null;       // Must be before transform(srcPts, …).
            if (dstPts != null) {
                tr.transform(srcPts, srcOff, dstPts, dstOff, 1);
            }
            return derivative;
        }
    }

    /**
     * Call of a {@code MathTransform.transform(…)} method with source and target arrays fixed at
     * {@code TransformCall} creation time. This is used for allowing the same implementation to
     * be shared by most {@code transform(…)} methods in {@link SpecializableTransform}.
     */
    @FunctionalInterface
    private interface TransformCall {
        /** Performs the transform at the given offsets. */
        void apply(MathTransform tr, int srcOff, int dstOff, int numPts) throws TransformException;
    }

    /**
     * Transforms a list of coordinate points. This method delegates to the most specialized transform,
     * with single {@code transform(…)} calls for coordinate sequences as long as possible.
     *
     * @param  transform  caller for a {@code MathTransform.transform(…)} method.
     * @param  src        a window over the source points. May be backed by a {@code float[]} or {@code double[]} array.
     * @param  dstOff     where to write the first coordinate in the target array.
     * @param  srcInc     the source dimension, negative if we must iterate backward.
     * @param  dstInc     the target dimension, negative if we must iterate backward.
     * @param  numPts     number of points to transform.
     */
    private void transform(final TransformCall transform, final DirectPositionView src,
            int dstOff, int srcInc, int dstInc, int numPts) throws TransformException
    {
        final boolean downard = (srcInc < 0);
        SubArea domain = SubArea.find(domains, src);
        while (numPts > 0) {
            int srcOff = src.offset;
            final MathTransform tr;
            if (domain == null) {
                tr = global;                               // The transform to apply when no specialization is found.
                do {                                        // Count how many points will use that transform.
                    src.offset += srcInc;
                    if (--numPts <= 0) break;
                    domain = SubArea.find(domains, src);    // More expansive check than the case where domain is non-null.
                } while (domain == null);
            } else {
                final SubArea previous = domain;
                tr = domain.transform;                      // The specialized transform to apply.
                do {                                        // Count how many points will use that transform.
                    src.offset += srcInc;
                    if (--numPts <= 0) break;
                    domain = SubArea.find(domain, src);     // Cheaper check compared to the case where domain is null.
                } while (domain == previous);
                if (domain == null) {
                    domain = SubArea.find(domains, src);    // Need to update with the more expansive check.
                }
            }
            final int num = (src.offset - srcOff) / srcInc;
            int dstLow = dstOff;
            dstOff += dstInc * num;
            if (downard) {
                srcOff = src.offset - srcInc;
                dstLow = dstOff     - dstInc;
            }
            transform.apply(tr, srcOff, dstLow, num);
        }
    }

    /**
     * Transforms a list of coordinate points.
     * This method delegates to the most specialized transform.
     */
    @Override
    public final void transform(double[] srcPts, int srcOff, final double[] dstPts, int dstOff, final int numPts)
            throws TransformException
    {
        int srcInc = getSourceDimensions();
        int dstInc = getTargetDimensions();
        if (srcPts == dstPts) {
            switch (IterationStrategy.suggest(srcOff, srcInc, dstOff, dstInc, numPts)) {
                case ASCENDING: {
                    break;
                }
                case DESCENDING: {
                    srcOff += (numPts-1) * srcInc; srcInc = -srcInc;
                    dstOff += (numPts-1) * dstInc; dstInc = -dstInc;
                    break;
                }
                default: {
                    srcPts = Arrays.copyOfRange(srcPts, srcOff, srcOff + numPts*srcInc);
                    srcOff = 0;
                    break;
                }
            }
        }
        final double[] refPts = srcPts;
        transform((tr, src, dst, num) -> tr.transform(refPts, src, dstPts, dst, num),
                  new DirectPositionView.Double(srcPts, srcOff, Math.abs(srcInc)),
                  dstOff, srcInc, dstInc, numPts);
    }

    /**
     * Transforms a list of coordinate points.
     * This method delegates to the most specialized transform.
     */
    @Override
    public final void transform(float[] srcPts, int srcOff, final float[] dstPts, int dstOff, final int numPts)
            throws TransformException
    {
        int srcInc = getSourceDimensions();
        int dstInc = getTargetDimensions();
        if (srcPts == dstPts) {
            switch (IterationStrategy.suggest(srcOff, srcInc, dstOff, dstInc, numPts)) {
                case ASCENDING: {
                    break;
                }
                case DESCENDING: {
                    srcOff += (numPts-1) * srcInc; srcInc = -srcInc;
                    dstOff += (numPts-1) * dstInc; dstInc = -dstInc;
                    break;
                }
                default: {
                    srcPts = Arrays.copyOfRange(srcPts, srcOff, srcOff + numPts*srcInc);
                    srcOff = 0;
                    break;
                }
            }
        }
        final float[] refPts = srcPts;
        transform((tr, src, dst, num) -> tr.transform(refPts, src, dstPts, dst, num),
                  new DirectPositionView.Float(srcPts, srcOff, Math.abs(srcInc)),
                  dstOff, srcInc, dstInc, numPts);
    }

    /**
     * Transforms a list of coordinate points. This method delegates to the most specialized transform,
     * with single {@code transform(…)} calls for coordinate sequences as long as possible.
     */
    @Override
    public final void transform(final double[] srcPts, final int srcOff,
                                final float [] dstPts, final int dstOff,
                                final int numPts) throws TransformException
    {
        final int srcDim = getSourceDimensions();
        final int dstDim = getTargetDimensions();
        transform((tr, src, dst, num) -> tr.transform(srcPts, src, dstPts, dst, num),
                  new DirectPositionView.Double(srcPts, srcOff, srcDim),
                  dstOff, srcDim, dstDim, numPts);
    }

    /**
     * Transforms a list of coordinate points. This method delegates to the most specialized transform,
     * with single {@code transform(…)} calls for coordinate sequences as long as possible.
     */
    @Override
    public final void transform(final float [] srcPts, final int srcOff,
                                final double[] dstPts, final int dstOff,
                                final int numPts) throws TransformException
    {
        final int srcDim = getSourceDimensions();
        final int dstDim = getTargetDimensions();
        transform((tr, src, dst, num) -> tr.transform(srcPts, src, dstPts, dst, num),
                  new DirectPositionView.Float(srcPts, srcOff, srcDim),
                  dstOff, srcDim, dstDim, numPts);
    }

    /**
     * Computes a hash value for this transform.
     * This method is invoked by {@link #hashCode()} when first needed.
     */
    @Override
    protected final int computeHashCode() {
        return super.computeHashCode() + 7*global.hashCode() ^ Arrays.hashCode(domains);
    }

    /**
     * Compares the specified object with this math transform for equality.
     */
    @Override
    public final boolean equals(final Object object, final ComparisonMode mode) {
        if (super.equals(object, mode)) {
            final SpecializableTransform other = (SpecializableTransform) object;
            return Utilities.deepEquals(global, other.global, mode) &&
                   Utilities.deepEquals(domains, other.domains, mode);
        }
        return false;
    }

    /**
     * Formats the inner part of a <cite>Well Known Text</cite> version 1 (WKT 1) element.
     *
     * <div class="note"><b>Compatibility note:</b>
     * The {@code SPECIALIZABLE_MT} element formatted here is an Apache SIS-specific extension.</div>
     *
     * @param  formatter  the formatter to use.
     * @return the WKT element name, which is {@code "Specializable_MT"}.
     */
    @Override
    protected final String formatTo(final Formatter formatter) {
        formatter.newLine();
        formatter.append(global);
        for (SubArea domain : domains) {
            SubArea.format(domain, formatter);
        }
        formatter.setInvalidWKT(SpecializableTransform.class, null);
        return "Specializable_MT";
    }

    /**
     * Returns the inverse of this transform.
     */
    @Override
    public synchronized MathTransform inverse() throws NoninvertibleTransformException {
        if (inverse == null) {
            inverse = createInverse();
        }
        return inverse;
    }

    /**
     * Invoked at construction time for creating the inverse transform.
     * Overridden by {@link SpecializableTransform2D} for the two-dimensional variant.
     */
    Inverse createInverse() throws NoninvertibleTransformException {
        return new Inverse(this);
    }

    /**
     * The inverse of {@link SpecializableTransform}.
     */
    static class Inverse extends AbstractMathTransform.Inverse implements Serializable {
        /**
         * For cross-version compatibility.
         */
        private static final long serialVersionUID = 1060617594604917167L;

        /**
         * The enclosing transform.
         */
        private final SpecializableTransform forward;

        /**
         * The inverse of {@link SpecializableTransform#global}.
         */
        private final MathTransform global;

        /**
         * Creates the inverse of a specialized transform having the given properties.
         */
        Inverse(final SpecializableTransform forward) throws NoninvertibleTransformException {
            this.forward = forward;
            this.global = forward.global.inverse();
            for (final SubArea domain : forward.domains) {
                SubArea.createInverse(domain);
            }
        }

        /**
         * Returns the inverse of this math transform.
         */
        @Override
        public MathTransform inverse() {
            return forward;
        }

        /**
         * Inverse transforms the specified {@code ptSrc} and stores the result in {@code ptDst}.
         */
        @Override
        public final DirectPosition transform(final DirectPosition ptSrc, DirectPosition ptDst) throws TransformException {
            final double[] source = ptSrc.getCoordinate();      // Needs to be first in case ptDst overwrites ptSrc.
            ptDst = global.transform(ptSrc, ptDst);
            final SubArea domain = SubArea.find(forward.domains, ptDst);
            if (domain != null) {
                ptDst = domain.inverse.transform(new DirectPositionView.Double(source), ptDst);
            }
            return ptDst;
        }

        /**
         * Gets the inverse derivative of this transform at a point.
         * This method is overridden for consistency.
         */
        @Override
        public final Matrix derivative(final DirectPosition point) throws TransformException {
            return transform(point.getCoordinate(), 0, null, 0, true);
        }

        /**
         * Inverse transforms a single coordinate point in an array, and optionally computes the transform
         * derivative at that location.
         */
        @Override
        public final Matrix transform(double[] srcPts, int srcOff, double[] dstPts, int dstOff, final boolean derivate)
                throws TransformException
        {
            final int srcInc = global.getSourceDimensions();
            final int dstInc = global.getTargetDimensions();
            if (dstPts == null) {
                dstPts = new double[dstInc];                    // Needed for checking if inside a sub-area.
                dstOff = 0;
            } else if (srcPts == dstPts && srcOff + srcInc > dstOff && srcOff < dstOff + dstInc) {
                srcPts = Arrays.copyOfRange(srcPts, srcOff, srcInc);
                srcOff = 0;
            }
            /*
             * Above 'srcPts' dhould keep the source coordinates unchanged even if the source and destination
             * given in arguments overlap.  We need this stability because the source coordinates may be used
             * twice, if 'secondTry' become true.
             */
            MathTransform tr = global;
            boolean secondTry = false;
            Matrix derivative;
            do {
                if (tr instanceof AbstractMathTransform) {
                    derivative = ((AbstractMathTransform) tr).transform(srcPts, srcOff, dstPts, dstOff, derivate);
                } else {
                    tr.transform(srcPts, srcOff, dstPts, dstOff, 1);
                    derivative = derivate ? tr.derivative(new DirectPositionView.Double(srcPts, srcOff, srcInc)) : null;
                }
                if (secondTry) break;
                final SubArea domain = SubArea.find(forward.domains, new DirectPositionView.Double(dstPts, dstOff, dstInc));
                if (domain != null) {
                    tr = domain.inverse;
                    secondTry = true;
                }
            } while (secondTry);
            return derivative;
        }

        /**
         * Invoked for transforming, then verifying if more appropriate transform exists for the result.
         * This implementation is similar to the algorithm applied by {@link SpecializableTransform} parent
         * class, except that {@link SubArea} is verified <em>after</em> transformations instead than before.
         */
        private void transform(final TransformCall transform, final double[] dstPts,
                int srcOff, int dstOff, int srcInc, int dstInc, int numPts) throws TransformException
        {
            final SubArea[] domains = forward.domains;
            transform.apply(global, srcOff, dstOff, numPts);
            final DirectPositionView dst = new DirectPositionView.Double(dstPts, dstOff, dstInc);
            while (numPts > 0) {
                SubArea domain = SubArea.find(domains, dst);
                if (domain == null) {
                    dst.offset += dstInc;
                    numPts--;
                    continue;
                }
                do {
                    final SubArea specialized = domain;             // Contains the specialized transform to use.
                    int num = (dst.offset - dstOff) / dstInc;       // Number of points that are not retransformeD.
                    srcOff += num * srcInc;                         // Skip the source coordinates that are not retransformed.
                    dstOff = dst.offset;                            // Destination index of the first coordinate to retransform.
                    do {
                        dst.offset += dstInc;                       // Destination index after the last coordinate to transform.
                        if (--numPts <= 0) {
                            domain = null;
                            break;
                        }
                        domain = SubArea.find(domain, dst);
                    } while (domain == specialized);
                    num = (dst.offset - dstOff) / dstInc;           // Number of points to retransform.
                    transform.apply(specialized.inverse, srcOff, dstOff, num);
                    srcOff += srcInc * num;
                    dstOff = dst.offset;
                } while (domain != null);
            }
        }

        /**
         * Inverse transforms a list of coordinate points.
         * The transformed points are written directly in the destination array.
         */
        @Override
        public void transform(double[] srcPts, int srcOff, final double[] dstPts, final int dstOff, final int numPts)
                throws TransformException
        {
            if (numPts <= 0) return;
            final int srcInc = global.getSourceDimensions();
            final int dstInc = global.getTargetDimensions();
            if (srcPts == dstPts) {
                final int srcEnd = srcOff + numPts*srcInc;
                if (srcEnd > dstOff || dstOff + numPts*dstInc > srcOff) {
                    srcPts = Arrays.copyOfRange(srcPts, srcOff, srcEnd);
                    srcOff = 0;
                }
            }
            final double[] refPts = srcPts;
            transform((tr, src, dst, num) -> tr.transform(refPts, src, dstPts, dst, num),
                      dstPts, srcOff, dstOff, srcInc, dstInc, numPts);
        }

        /**
         * Inverse transforms a list of coordinate points. This method uses an temporary {@code double[]} buffer
         * for testing {@code SubArea} inclusion with full precision before to cast to {@code float} values.
         */
        @Override
        public void transform(final float[] srcPts, int srcOff,
                              final float[] dstPts, int dstOff, int numPts)
                throws TransformException
        {
            if (numPts <= 0) return;
            final int srcInc = global.getSourceDimensions();
            final int dstInc = global.getTargetDimensions();
            final double[] buffer = new double[numPts * dstInc];
            transform((tr, src, dst, num) -> tr.transform(srcPts, src, buffer, dst, num),
                      buffer, srcOff, 0, srcInc, dstInc, numPts);

            numPts *= dstInc;
            for (int i=0; i<numPts; i++) {
                dstPts[dstOff++] = (float) buffer[i];
            }
        }

        /**
         * Inverse transforms a list of coordinate points. This method uses an temporary {@code double[]} buffer
         * for testing {@code SubArea} inclusion with full precision before to cast to {@code float} values.
         */
        @Override
        public void transform(final double[] srcPts, int srcOff,
                              final float [] dstPts, int dstOff, int numPts) throws TransformException
        {
            if (numPts <= 0) return;
            final int srcInc = global.getSourceDimensions();
            final int dstInc = global.getTargetDimensions();
            final double[] buffer = new double[numPts * dstInc];
            transform((tr, src, dst, num) -> tr.transform(srcPts, src, buffer, dst, num),
                      buffer, srcOff, 0, srcInc, dstInc, numPts);

            numPts *= dstInc;
            for (int i=0; i<numPts; i++) {
                dstPts[dstOff++] = (float) buffer[i];
            }
        }

        /**
         * Inverse transforms a list of coordinate points.
         * The transformed points are written directly in the destination array.
         */
        @Override
        public void transform(final float [] srcPts, int srcOff,
                              final double[] dstPts, int dstOff, int numPts) throws TransformException
        {
            if (numPts <= 0) return;
            final int srcInc = global.getSourceDimensions();
            final int dstInc = global.getTargetDimensions();
            transform((tr, src, dst, num) -> tr.transform(srcPts, src, dstPts, dst, num),
                      dstPts, srcOff, dstOff, srcInc, dstInc, numPts);
        }
    }
}
