/*
 *    GeoTools - The Open Source Java GIS Toolkit
 *    http://geotools.org
 *
 *    (C) 2001-2008, Open Source Geospatial Foundation (OSGeo)
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation;
 *    version 2.1 of the License.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 *
 *    This package contains documentation from OpenGIS specifications.
 *    OpenGIS consortium's work is fully acknowledged here.
 */
package org.geotools.referencing.operation;

import java.util.Map;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Collections;
import java.awt.RenderingHints;
import javax.measure.converter.ConversionException;

import org.opengis.metadata.quality.PositionalAccuracy;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.IdentifiedObject;
import org.opengis.referencing.ReferenceIdentifier;
import org.opengis.referencing.cs.CoordinateSystem;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.*;
import static org.opengis.referencing.IdentifiedObject.NAME_KEY;

import org.geotools.factory.Hints;
import org.geotools.metadata.iso.citation.Citations;
import org.geotools.metadata.iso.quality.PositionalAccuracyImpl;
import org.geotools.referencing.AbstractIdentifiedObject;
import org.geotools.referencing.NamedIdentifier;
import org.geotools.referencing.factory.ReferencingFactory;
import org.geotools.referencing.factory.ReferencingFactoryContainer;
import org.geotools.referencing.cs.AbstractCS;
import org.geotools.referencing.operation.transform.ProjectiveTransform;
import org.geotools.resources.Classes;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Vocabulary;
import org.geotools.resources.i18n.VocabularyKeys;
import org.geotools.util.CanonicalSet;
import org.geotools.util.Utilities;

import static org.geotools.referencing.CRS.equalsIgnoreMetadata;


/**
 * Base class for coordinate operation factories. This class provides helper methods for the
 * construction of building blocks. It doesn't figure out any operation path by itself. This
 * more "intelligent" job is left to subclasses.
 *
 * @since 2.1
 *
 * @source $URL: http://svn.osgeo.org/geotools/branches/2.7.x/modules/library/referencing/src/main/java/org/geotools/referencing/operation/AbstractCoordinateOperationFactory.java $
 * @version $Id: AbstractCoordinateOperationFactory.java 37299 2011-05-25 05:21:24Z mbedward $
 * @author Martin Desruisseaux (IRD)
 */
public abstract class AbstractCoordinateOperationFactory extends ReferencingFactory
        implements CoordinateOperationFactory
{
    /**
     * The identifier for an identity operation.
     */
    protected static final ReferenceIdentifier IDENTITY =
            new NamedIdentifier(Citations.GEOTOOLS,
                Vocabulary.formatInternational(VocabularyKeys.IDENTITY));

    /**
     * The identifier for conversion using an affine transform for axis swapping and/or
     * unit conversions.
     */
    protected static final ReferenceIdentifier AXIS_CHANGES =
            new NamedIdentifier(Citations.GEOTOOLS,
                Vocabulary.formatInternational(VocabularyKeys.AXIS_CHANGES));

    /**
     * The identifier for a transformation which is a datum shift.
     *
     * @see PositionalAccuracyImpl#DATUM_SHIFT_APPLIED
     */
    protected static final ReferenceIdentifier DATUM_SHIFT =
            new NamedIdentifier(Citations.GEOTOOLS,
                Vocabulary.formatInternational(VocabularyKeys.DATUM_SHIFT));

    /**
     * The identifier for a transformation which is a datum shift without
     * {@linkplain org.geotools.referencing.datum.BursaWolfParameters Bursa Wolf parameters}.
     * Only the changes in ellipsoid axis-length are taken in account. Such ellipsoid shifts
     * are approximative and may have 1 kilometer error. This transformation is allowed
     * only if the factory was created with {@link Hints#LENIENT_DATUM_SHIFT} set to
     * {@link Boolean#TRUE}.
     *
     * @see PositionalAccuracyImpl#DATUM_SHIFT_OMITTED
     */
    protected static final ReferenceIdentifier ELLIPSOID_SHIFT =
            new NamedIdentifier(Citations.GEOTOOLS,
                Vocabulary.formatInternational(VocabularyKeys.ELLIPSOID_SHIFT));

    /**
     * The identifier for a geocentric conversion.
     */
    protected static final ReferenceIdentifier GEOCENTRIC_CONVERSION =
            new NamedIdentifier(Citations.GEOTOOLS,
                Vocabulary.formatInternational(VocabularyKeys.GEOCENTRIC_TRANSFORM));

    /**
     * The identifier for an inverse operation.
     */
    protected static final ReferenceIdentifier INVERSE_OPERATION =
            new NamedIdentifier(Citations.GEOTOOLS,
                Vocabulary.formatInternational(VocabularyKeys.INVERSE_OPERATION));

    /**
     * The set of helper methods on factories.
     *
     * @see #getFactoryGroup
     */
    private final ReferencingFactoryContainer factories;

    /**
     * The underlying math transform factory. This factory is used
     * for constructing {@link MathTransform} objects for all
     * {@linkplain CoordinateOperation coordinate operations}.
     *
     * @see #getMathTransformFactory
     */
    private final MathTransformFactory mtFactory;

    /**
     * A pool of coordinate operation. This pool is used in order
     * to returns instance of existing operations when possible.
     */
    private final CanonicalSet<CoordinateOperation> pool =
            CanonicalSet.newInstance(CoordinateOperation.class);

    /**
     * Tells if {@link FactoryGroup#hints} has been invoked. It must be invoked exactly once,
     * but can't be invoked in the constructor because it causes a {@link StackOverflowError}
     * in some situations.
     */
    private boolean hintsInitialized;

    /**
     * Constructs a coordinate operation factory using the specified hints and priority.
     * This constructor recognizes the {@link Hints#CRS_FACTORY CRS}, {@link Hints#CS_FACTORY CS},
     * {@link Hints#DATUM_FACTORY DATUM} and {@link Hints#MATH_TRANSFORM_FACTORY MATH_TRANSFORM}
     * {@code FACTORY} hints.
     *
     * @param userHints The hints, or {@code null} if none.
     * @param priority The priority for this factory, as a number between
     *        {@link #MINIMUM_PRIORITY MINIMUM_PRIORITY} and
     *        {@link #MAXIMUM_PRIORITY MAXIMUM_PRIORITY} inclusive.
     *
     * @since 2.2
     */
    public AbstractCoordinateOperationFactory(final Hints userHints, final int priority) {
        super(priority);
        factories = ReferencingFactoryContainer.instance(userHints);
        mtFactory = factories.getMathTransformFactory();
    }

    /**
     * If the specified factory is an instance of {@code AbstractCoordinateOperationFactory},
     * fetch the {@link FactoryGroup} from this instance instead of from the hints. This
     * constructor is strictly reserved for factory subclasses that are wrapper around an
     * other factory, like {@link BufferedCoordinateOperationFactory}.
     */
    AbstractCoordinateOperationFactory(final CoordinateOperationFactory factory,
                                       final Hints hints, final int priority)
    {
        super(priority);
        if (factory instanceof AbstractCoordinateOperationFactory) {
            factories = ((AbstractCoordinateOperationFactory) factory).getFactoryContainer();
        } else {
            factories = ReferencingFactoryContainer.instance(hints);
        }
        mtFactory = factories.getMathTransformFactory();
    }

    /**
     * Returns the implementation hints for this factory. The returned map contains values for
     * {@link Hints#CRS_FACTORY CRS}, {@link Hints#CS_FACTORY CS}, {@link Hints#DATUM_FACTORY DATUM}
     * and {@link Hints#MATH_TRANSFORM_FACTORY MATH_TRANSFORM} {@code FACTORY} hints. Other values
     * may be provided as well, at implementation choice.
     */
    @Override
    public Map<RenderingHints.Key,?> getImplementationHints() {
        synchronized (hints) { // Note: avoid lock on public object.
            if (!hintsInitialized) {
                initializeHints();
                hintsInitialized = true; // Set only after success.
            }
        }
        return super.getImplementationHints();
    }

    /**
     * Invoked when the {@link #hints} map should be initialized. This method may
     * be overridden by subclasses like {@link BufferedCoordinateOperationFactory}.
     */
    void initializeHints() {
        assert Thread.holdsLock(hints);
        final ReferencingFactoryContainer factories = getFactoryContainer();
        hints.putAll(factories.getImplementationHints());
    }

    /**
     * Returns the underlying math transform factory. This factory
     * is used for constructing {@link MathTransform} objects for
     * all {@linkplain CoordinateOperation coordinate operations}.
     *
     * @return The underlying math transform factory.
     */
    public final MathTransformFactory getMathTransformFactory() {
        return mtFactory;
    }

    /**
     * Returns the set of helper methods on factories.
     */
    final ReferencingFactoryContainer getFactoryContainer() {
        return factories;
    }

    /**
     * Returns an affine transform between two coordinate systems. Only units and
     * axis order (e.g. transforming from (NORTH,WEST) to (EAST,NORTH)) are taken
     * in account.
     * <p>
     * Example: If coordinates in {@code sourceCS} are (x,y) pairs in metres and
     * coordinates in {@code targetCS} are (-y,x) pairs in centimetres, then the
     * transformation can be performed as below:
     *
     * <pre><blockquote>
     *          [-y(cm)]   [ 0  -100    0 ] [x(m)]
     *          [ x(cm)] = [ 100   0    0 ] [y(m)]
     *          [ 1    ]   [ 0     0    1 ] [1   ]
     * </blockquote></pre>
     *
     * @param  sourceCS The source coordinate system.
     * @param  targetCS The target coordinate system.
     * @return The transformation from {@code sourceCS} to {@code targetCS} as
     *         an affine transform. Only axis orientation and units are taken in account.
     * @throws OperationNotFoundException If the affine transform can't be constructed.
     *
     * @see AbstractCS#swapAndScaleAxis
     */
    protected Matrix swapAndScaleAxis(final CoordinateSystem sourceCS,
                                      final CoordinateSystem targetCS)
            throws OperationNotFoundException
    {
        try {
            return AbstractCS.swapAndScaleAxis(sourceCS,targetCS);
        } catch (IllegalArgumentException exception) {
            throw new OperationNotFoundException(getErrorMessage(sourceCS, targetCS), exception);
        } catch (ConversionException exception) {
            throw new OperationNotFoundException(getErrorMessage(sourceCS, targetCS), exception);
        }
        // No attempt to catch ClassCastException since such
        // exception would indicates a programming error.
    }

    /**
     * Returns the specified identifier in a map to be given to coordinate operation constructors.
     * In the special case where the {@code name} identifier is {@link #DATUM_SHIFT} or
     * {@link #ELLIPSOID_SHIFT}, the map will contains extra informations like positional
     * accuracy.
     *
     * @todo In the datum shift case, an operation version is mandatory but unknow at this time.
     *       However, we noticed that the EPSG database do not always defines a version neither.
     *       Consequently, the Geotools implementation relax the rule requirying an operation
     *       version and we do not try to provide this information here for now.
     */
    private static Map<String,Object> getProperties(final ReferenceIdentifier name) {
        final Map<String,Object> properties;
        if (name==DATUM_SHIFT || name==ELLIPSOID_SHIFT) {
            properties = new HashMap<String,Object>(4);
            properties.put(NAME_KEY, name);
            properties.put(CoordinateOperation.COORDINATE_OPERATION_ACCURACY_KEY,
                  new PositionalAccuracy[] {
                      name==DATUM_SHIFT ? PositionalAccuracyImpl.DATUM_SHIFT_APPLIED
                                        : PositionalAccuracyImpl.DATUM_SHIFT_OMITTED});
        } else {
            properties = Collections.singletonMap(NAME_KEY, (Object) name);
        }
        return properties;
    }

    /**
     * Creates a coordinate operation from a matrix, which usually describes an affine tranform.
     * A default {@link OperationMethod} object is given to this transform. In the special case
     * where the {@code name} identifier is {@link #DATUM_SHIFT} or {@link #ELLIPSOID_SHIFT},
     * the operation will be an instance of {@link Transformation} instead of the usual
     * {@link Conversion}.
     *
     * @param  name      The identifier for the operation to be created.
     * @param  sourceCRS The source coordinate reference system.
     * @param  targetCRS The target coordinate reference system.
     * @param  matrix    The matrix which describe an affine transform operation.
     * @return The conversion or transformation.
     * @throws FactoryException if the operation can't be created.
     */
    protected CoordinateOperation createFromAffineTransform(
                                  final ReferenceIdentifier       name,
                                  final CoordinateReferenceSystem sourceCRS,
                                  final CoordinateReferenceSystem targetCRS,
                                  final Matrix                    matrix)
            throws FactoryException
    {
        final MathTransform transform = mtFactory.createAffineTransform(matrix);
        final Map<String,?> properties = getProperties(name);
        final Class<? extends Operation> type =
                properties.containsKey(CoordinateOperation.COORDINATE_OPERATION_ACCURACY_KEY)
                           ? Transformation.class : Conversion.class;
        return createFromMathTransform(properties, sourceCRS, targetCRS, transform,
               ProjectiveTransform.ProviderAffine.getProvider(transform.getSourceDimensions(),
                                                              transform.getTargetDimensions()), type);
    }

    /**
     * Creates a coordinate operation from a set of parameters.
     * The {@linkplain OperationMethod operation method} is inferred automatically,
     * if possible.
     *
     * @param  name       The identifier for the operation to be created.
     * @param  sourceCRS  The source coordinate reference system.
     * @param  targetCRS  The target coordinate reference system.
     * @param  parameters The parameters.
     * @return The conversion or transformation.
     * @throws FactoryException if the operation can't be created.
     */
    protected CoordinateOperation createFromParameters(
                                  final ReferenceIdentifier       name,
                                  final CoordinateReferenceSystem sourceCRS,
                                  final CoordinateReferenceSystem targetCRS,
                                  final ParameterValueGroup       parameters)
            throws FactoryException
    {
        final Map<String,?> properties = getProperties(name);
        final MathTransform transform = mtFactory.createParameterizedTransform(parameters);
        final OperationMethod  method = mtFactory.getLastMethodUsed();
        return createFromMathTransform(properties, sourceCRS, targetCRS, transform,
                                       method, Operation.class);
    }

    /**
     * Creates a coordinate operation from a math transform.
     *
     * @param  name       The identifier for the operation to be created.
     * @param  sourceCRS  The source coordinate reference system.
     * @param  targetCRS  The destination coordinate reference system.
     * @param  transform  The math transform.
     * @return A coordinate operation using the specified math transform.
     * @throws FactoryException if the operation can't be constructed.
     */
    protected CoordinateOperation createFromMathTransform(
                                  final ReferenceIdentifier       name,
                                  final CoordinateReferenceSystem sourceCRS,
                                  final CoordinateReferenceSystem targetCRS,
                                  final MathTransform             transform)
            throws FactoryException
    {
        return createFromMathTransform(Collections.singletonMap(NAME_KEY, name),
                                       sourceCRS, targetCRS, transform, null,
                                       CoordinateOperation.class);
    }

    /**
     * Creates a coordinate operation from a math transform.
     * If the specified math transform is already a coordinate operation, and if source
     * and target CRS match, then {@code transform} is returned with no change.
     * Otherwise, a new coordinate operation is created.
     *
     * @param  properties The properties to give to the operation.
     * @param  sourceCRS  The source coordinate reference system.
     * @param  targetCRS  The destination coordinate reference system.
     * @param  transform  The math transform.
     * @param  method     The operation method, or {@code null}.
     * @param  type       The required super-class (e.g. <code>{@linkplain Transformation}.class</code>).
     * @return A coordinate operation using the specified math transform.
     * @throws FactoryException if the operation can't be constructed.
     */
    protected CoordinateOperation createFromMathTransform(
                                  final Map<String,?>             properties,
                                  final CoordinateReferenceSystem sourceCRS,
                                  final CoordinateReferenceSystem targetCRS,
                                  final MathTransform             transform,
                                  final OperationMethod           method,
                                  final Class<? extends CoordinateOperation> type)
            throws FactoryException
    {
        CoordinateOperation operation;
        if (transform instanceof CoordinateOperation) {
            operation = (CoordinateOperation) transform;
            if (Utilities.equals(operation.getSourceCRS(),     sourceCRS) &&
                Utilities.equals(operation.getTargetCRS(),     targetCRS) &&
                Utilities.equals(operation.getMathTransform(), transform))
            {
                if (operation instanceof Operation) {
                    if (Utilities.equals(((Operation) operation).getMethod(), method)) {
                        return operation;
                    }
                } else {
                    return operation;
                }
            }
        }
        operation = DefaultOperation.create(properties, sourceCRS, targetCRS, transform, method, type);
        operation = pool.unique(operation);
        return operation;
    }

    /**
     * Creates a concatenated operation from a sequence of operations.
     *
     * @param  properties Set of properties. Should contains at least {@code "name"}.
     * @param  operations The sequence of operations.
     * @return The concatenated operation.
     * @throws FactoryException if the object creation failed.
     */
    public CoordinateOperation createConcatenatedOperation(
                        final Map<String,?> properties,
                        final CoordinateOperation[] operations) throws FactoryException
    {
        CoordinateOperation operation;
        operation = new DefaultConcatenatedOperation(properties, operations, mtFactory);
        operation = pool.unique(operation);
        return operation;
    }

    /**
     * Concatenate two operation steps. If an operation is an {@link #AXIS_CHANGES},
     * it will be included as part of the second operation instead of creating an
     * {@link ConcatenatedOperation}. If a concatenated operation is created, it
     * will get an automatically generated name.
     *
     * @param  step1 The first  step, or {@code null} for the identity operation.
     * @param  step2 The second step, or {@code null} for the identity operation.
     * @return A concatenated operation, or {@code null} if all arguments was nul.
     * @throws FactoryException if the operation can't be constructed.
     */
    protected CoordinateOperation concatenate(final CoordinateOperation step1,
                                              final CoordinateOperation step2)
            throws FactoryException
    {
        if (step1 == null) return step2;
        if (step2 == null) return step1;
        if (isIdentity(step1)) return step2;
        if (isIdentity(step2)) return step1;
        final MathTransform mt1 = step1.getMathTransform();
        final MathTransform mt2 = step2.getMathTransform();
        final CoordinateReferenceSystem sourceCRS = step1.getSourceCRS();
        final CoordinateReferenceSystem targetCRS = step2.getTargetCRS();
        CoordinateOperation step = null;
        if (step1.getName()==AXIS_CHANGES && mt1.getSourceDimensions()==mt1.getTargetDimensions()) step = step2;
        if (step2.getName()==AXIS_CHANGES && mt2.getSourceDimensions()==mt2.getTargetDimensions()) step = step1;
        if (step instanceof Operation) {
            /*
             * Applies only on operation in order to avoid merging with PassThroughOperation.
             * Also applies only if the transform to hide has identical source and target
             * dimensions in order to avoid mismatch with the method's dimensions.
             */
            return createFromMathTransform(AbstractIdentifiedObject.getProperties(step),
                   sourceCRS, targetCRS, mtFactory.createConcatenatedTransform(mt1, mt2),
                   ((Operation) step).getMethod(), CoordinateOperation.class);
        }
        return createConcatenatedOperation(getTemporaryName(sourceCRS, targetCRS),
                                           new CoordinateOperation[] {step1, step2});
    }

    /**
     * Concatenate three transformation steps. If the first and/or the last operation is an
     * {@link #AXIS_CHANGES}, it will be included as part of the second operation instead of
     * creating an {@link ConcatenatedOperation}. If a concatenated operation is created, it
     * will get an automatically generated name.
     *
     * @param  step1 The first  step, or {@code null} for the identity operation.
     * @param  step2 The second step, or {@code null} for the identity operation.
     * @param  step3 The third  step, or {@code null} for the identity operation.
     * @return A concatenated operation, or {@code null} if all arguments were null.
     * @throws FactoryException if the operation can't be constructed.
     */
    protected CoordinateOperation concatenate(final CoordinateOperation step1,
                                              final CoordinateOperation step2,
                                              final CoordinateOperation step3)
            throws FactoryException
    {
        if (step1 == null) return concatenate(step2, step3);
        if (step2 == null) return concatenate(step1, step3);
        if (step3 == null) return concatenate(step1, step2);
        assert equalsIgnoreMetadata(step1.getTargetCRS(), step2.getSourceCRS()) : step1;
        assert equalsIgnoreMetadata(step2.getTargetCRS(), step3.getSourceCRS()) : step3;

        if (isIdentity(step1)) return concatenate(step2, step3);
        if (isIdentity(step2)) return concatenate(step1, step3);
        if (isIdentity(step3)) return concatenate(step1, step2);
        if (step1.getName() == AXIS_CHANGES) return concatenate(concatenate(step1, step2), step3);
        if (step3.getName() == AXIS_CHANGES) return concatenate(step1, concatenate(step2, step3));
        final CoordinateReferenceSystem sourceCRS = step1.getSourceCRS();
        final CoordinateReferenceSystem targetCRS = step3.getTargetCRS();
        return createConcatenatedOperation(getTemporaryName(sourceCRS, targetCRS),
                                           new CoordinateOperation[] {step1, step2, step3});
    }

    /**
     * Returns {@code true} if the specified operation is an identity conversion.
     * This method always returns {@code false} for transformations even if their
     * associated math transform is an identity one, because such transformations
     * are usually datum shift and must be visible.
     */
    private static boolean isIdentity(final CoordinateOperation operation) {
        return (operation instanceof Conversion) && operation.getMathTransform().isIdentity();
    }

    /**
     * Returns the inverse of the specified operation.
     *
     * @param  operation The operation to invert.
     * @return The inverse of {@code operation}.
     * @throws NoninvertibleTransformException if the operation is not invertible.
     * @throws FactoryException if the operation creation failed for an other reason.
     *
     * @since 2.3
     */
    protected CoordinateOperation inverse(final CoordinateOperation operation)
            throws NoninvertibleTransformException, FactoryException
    {
        final CoordinateReferenceSystem sourceCRS = operation.getSourceCRS();
        final CoordinateReferenceSystem targetCRS = operation.getTargetCRS();
        final Map<String,Object> properties = AbstractIdentifiedObject.getProperties(operation, null);
        properties.putAll(getTemporaryName(targetCRS, sourceCRS));
        if (operation instanceof ConcatenatedOperation) {
            final LinkedList<CoordinateOperation> inverted = new LinkedList<CoordinateOperation>();
            for (final CoordinateOperation op : ((ConcatenatedOperation) operation).getOperations()) {
                inverted.addFirst(inverse(op));
            }
            return createConcatenatedOperation(properties,
                    inverted.toArray(new CoordinateOperation[inverted.size()]));
        }
        final MathTransform transform = operation.getMathTransform().inverse();
        final Class<? extends CoordinateOperation> type = AbstractCoordinateOperation.getType(operation);
        final OperationMethod method = (operation instanceof Operation) ?
                                       ((Operation) operation).getMethod() : null;
        return createFromMathTransform(properties, targetCRS, sourceCRS, transform, method, type);
    }




    /////////////////////////////////////////////////////////////////////////////////
    /////////////////////////////////////////////////////////////////////////////////
    ////////////                                                         ////////////
    ////////////                M I S C E L L A N E O U S                ////////////
    ////////////                                                         ////////////
    /////////////////////////////////////////////////////////////////////////////////
    /////////////////////////////////////////////////////////////////////////////////

    /**
     * Returns the dimension of the specified coordinate system,
     * or {@code 0} if the coordinate system is null.
     */
    static int getDimension(final CoordinateReferenceSystem crs) {
        return (crs!=null) ? crs.getCoordinateSystem().getDimension() : 0;
    }

    /**
     * An identifier for temporary objects. This identifier manage a count of temporary
     * identifier. The count is appended to the identifier name (e.g. "WGS84 (step 1)").
     */
    private static final class TemporaryIdentifier extends NamedIdentifier {
        /** For cross-version compatibility. */
        private static final long serialVersionUID = -2784354058026177076L;

        /** The parent identifier. */
        private final ReferenceIdentifier parent;

        /** The temporary object count. */
        private final int count;

        /** Constructs an identifier derived from the specified one. */
        public TemporaryIdentifier(final ReferenceIdentifier parent) {
            this(parent, ((parent instanceof TemporaryIdentifier) ?
                         ((TemporaryIdentifier) parent).count : 0) + 1);
        }

        /** Work around for RFE #4093999 in Sun's bug database */
        private TemporaryIdentifier(final ReferenceIdentifier parent, final int count) {
            super(Citations.GEOTOOLS, unwrap(parent).getCode() + " (step " + count + ')');
            this.parent = parent;
            this.count  = count;
        }

        /** Returns the parent identifier for the specified identifier, if any. */
        public static ReferenceIdentifier unwrap(ReferenceIdentifier identifier) {
            while (identifier instanceof TemporaryIdentifier) {
                identifier = ((TemporaryIdentifier) identifier).parent;
            }
            return identifier;
        }
    }

    /**
     * Returns the name of the GeoAPI interface implemented by the specified object.
     * In addition, the name may be added between brackets.
     */
    private static String getClassName(final IdentifiedObject object) {
        if (object != null) {
            Class type = object.getClass();
            final Class[] interfaces = type.getInterfaces();
            for (int i=0; i<interfaces.length; i++) {
                final Class candidate = interfaces[i];
                if (candidate.getName().startsWith("org.opengis.referencing.")) {
                    type = candidate;
                    break;
                }
            }
            String name = Classes.getShortName(type);
            final ReferenceIdentifier id = object.getName();
            if (id != null) {
                name = name + '[' + id.getCode() + ']';
            }
            return name;
        }
        return null;
    }

    /**
     * Returns a temporary name for object derived from the specified one.
     *
     * @param source The CRS to base name on, or {@code null} if none.
     */
    static Map<String,Object> getTemporaryName(final IdentifiedObject source) {
        final Map<String,Object> properties = new HashMap<String,Object>(4);
        properties.put(NAME_KEY, new TemporaryIdentifier(source.getName()));
        properties.put(IdentifiedObject.REMARKS_KEY, Vocabulary.formatInternational(
                       VocabularyKeys.DERIVED_FROM_$1, getClassName(source)));
        return properties;
    }

    /**
     * Returns a temporary name for object derived from a concatenation.
     *
     * @param source The CRS to base name on, or {@code null} if none.
     */
    static Map<String,?> getTemporaryName(final CoordinateReferenceSystem source,
                                          final CoordinateReferenceSystem target)
    {
        final String name = getClassName(source) + " \u21E8 " + getClassName(target);
        return Collections.singletonMap(NAME_KEY, name);
    }

    /**
     * Returns an error message for "No path found from sourceCRS to targetCRS".
     * This is used for the construction of {@link OperationNotFoundException}.
     *
     * @param  source The source CRS.
     * @param  target The target CRS.
     * @return A default error message.
     */
    protected static String getErrorMessage(final IdentifiedObject source,
                                            final IdentifiedObject target)
    {
        return Errors.format(ErrorKeys.NO_TRANSFORMATION_PATH_$2,
                             getClassName(source), getClassName(target));
    }

    /**
     * Makes sure an argument is non-null.
     *
     * @param  name   Argument name.
     * @param  object User argument.
     * @throws IllegalArgumentException if {@code object} is null.
     */
    protected static void ensureNonNull(final String name, final Object object)
            throws IllegalArgumentException
    {
        if (object == null) {
            throw new IllegalArgumentException(Errors.format(ErrorKeys.NULL_ARGUMENT_$1, name));
        }
    }
}
