/*
 *    GeoTools - The Open Source Java GIS Toolkit
 *    http://geotools.org
 * 
 *    (C) 2006-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.
 */
package org.geotools.referencing.operation;

import java.util.Map;

import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.CoordinateOperation;
import org.opengis.referencing.operation.CoordinateOperationFactory;
import org.opengis.referencing.operation.OperationMethod;
import org.opengis.referencing.operation.OperationNotFoundException;

import org.geotools.factory.Hints;
import org.geotools.factory.BufferedFactory;
import org.geotools.util.Utilities;
import org.geotools.util.SoftValueHashMap;
import org.geotools.referencing.ReferencingFactoryFinder;


/**
 * Caches the {@linkplain CoordinateOperation coordinate operations} created by an other factory.
 * Those coordinate operations may be expensive to create. During rendering and during data I/O,
 * some implementations make use a lof of coordinate transformations, hence caching them might
 * help.
 * <p>
 * In most cases, users should not need to create an instance of this class explicitly. An instance
 * of {@code BufferedCoordinateOperationFactory} should be automatically registered and returned
 * by {@link ReferencingFactoryFinder} in default Geotools configuration.
 *
 * @since 2.3
 * @version $Id: BufferedCoordinateOperationFactory.java 37299 2011-05-25 05:21:24Z mbedward $
 *
 * @source $URL: http://svn.osgeo.org/geotools/branches/2.7.x/modules/library/referencing/src/main/java/org/geotools/referencing/operation/BufferedCoordinateOperationFactory.java $
 * @author Simone Giannecchini
 * @author Martin Desruisseaux
 */
public class BufferedCoordinateOperationFactory extends AbstractCoordinateOperationFactory
        implements BufferedFactory
{
    /**
     * The priority level for this factory.
     */
    static final int PRIORITY = AuthorityBackedFactory.PRIORITY + 10;

    /**
     * Helper class used in order to build an hashing for a pair of source-destination
     * {@link CoordinateReferenceSystem} objects. This is used to cache the transformations
     * that are pretty time-consuming to build each time.
     */
    private static final class CRSPair {
        /**
         * The hash code value, computed once for ever at construction time.
         */
        private final int hash;

        /**
         * The source and target CRS.
         */
        private final CoordinateReferenceSystem sourceCRS, targetCRS;

        /**
         * Creates a {@code CRSPair} for the specified source and target CRS.
         */
        public CRSPair(final CoordinateReferenceSystem sourceCRS,
                       final CoordinateReferenceSystem targetCRS)
        {
            this.sourceCRS = sourceCRS;
            this.targetCRS = targetCRS;
            this.hash = (37 * sourceCRS.hashCode()) + targetCRS.hashCode();
        }

        /**
         * Returns the hash code value.
         */
        @Override
        public int hashCode() {
            return hash;
        }

        /**
         * Compares this pair to the specified object for equality.
         * <p>
         * <strong>Note:</strong> we perform the CRS comparaison using strict equality, not using
         * {@code equalsIgnoreMetadata}, because metadata matter since they are attributes of the
         * {@link CoordinateOperation} object to be created.
         */
        @Override
        public boolean equals(final Object object) {
            if (object == this) {
                return true;
            }
            if (object instanceof CRSPair) {
                final CRSPair that = (CRSPair) object;
                return Utilities.equals(this.sourceCRS, that.sourceCRS) &&
                       Utilities.equals(this.targetCRS, that.targetCRS);
            }
            return false;
        }
    }

    /**
     * The wrapped factory. If {@code null}, will be fetched when first needed.
     * We should not initialize this field using {@link ReferencingFactoryFinder} from the
     * no-argument constructor, since this constructor is typically invoked while
     * {@link ReferencingFactoryFinder} is still iterating over the registered implementations.
     */
    private CoordinateOperationFactory factory;

    /**
     * The pool of cached transformations. This map can not be static, because the values may
     * be different for the same ({@code sourceCRS}, {@code targetCRS}) pair dependending of
     * hint values like {@link Hints#LENIENT_DATUM_SHIFT}.
     */
    private final Map<CRSPair, CoordinateOperation> pool =
            new SoftValueHashMap<CRSPair, CoordinateOperation>();

    /**
     * Creates a buffered factory wrapping the {@linkplain AuthorityBackedFactory default one}.
     */
    public BufferedCoordinateOperationFactory() {
        super(null, PRIORITY);
        /*
         * Do not use FactoryFinder here (directly or indirectly through the call
         * to an other constructor), because this constructor is typically invoked
         * while FactoryFinder is iterating over registered implementations. We
         * left the 'factory' field uninitialized and will initialize it when first
         * needed.
         */
    }

    /**
     * Creates a buffered factory wrapping an other factory selected according the specified hints.
     *
     * @param userHints The hints to use for choosing a backing factory.
     */
    public BufferedCoordinateOperationFactory(final Hints userHints) {
        this(userHints, PRIORITY);
    }

    /**
     * Creates a buffered factory wrapping an other factory selected according the specified hints.
     *
     * @param userHints The hints to use for choosing a backing factory.
     * @param priority The priority for this factory, as a number between
     *        {@link #MINIMUM_PRIORITY MINIMUM_PRIORITY} and
     *        {@link #MAXIMUM_PRIORITY MAXIMUM_PRIORITY} inclusive.
     */
    public BufferedCoordinateOperationFactory(final Hints userHints, final int priority) {
        this(getBackingFactory(userHints), userHints, priority);
    }

    /**
     * Work around for RFE #4093999 in Sun's bug database
     * ("Relax constraint on placement of this()/super() call in constructors").
     */
    private BufferedCoordinateOperationFactory(final CoordinateOperationFactory factory,
                                               final Hints userHints, final int priority)
    {
        super(factory, userHints, priority);
        this.factory = factory;
        ensureNonNull("factory", factory);
    }

    /**
     * Returns a backing factory from the specified hints.
     */
    private static CoordinateOperationFactory getBackingFactory(final Hints hints) {
        for (final CoordinateOperationFactory candidate : ReferencingFactoryFinder.getCoordinateOperationFactories(hints)) {
            if (!(candidate instanceof BufferedCoordinateOperationFactory)) {
                return candidate;
            }
        }
        // The following is likely to thrown a FactoryNotFoundException,
        // which is the intended behavior.
        return ReferencingFactoryFinder.getCoordinateOperationFactory(hints);
    }

    /**
     * Returns the backing factory. Coordinate operation creation will be delegated to this
     * factory when not available in the cache.
     */
    private final CoordinateOperationFactory getBackingFactory() {
        assert Thread.holdsLock(hints); // Same lock than the one used by getImplementationHints().
        if (factory == null) {
            factory = getBackingFactory(null);
        }
        return factory;
    }

    /**
     * Invoked by {@link #AbstractCoordinateOperationFactory} when the {@link #hints} map should
     * be initialized. The {@link Hints#COORDINATE_OPERATION_FACTORY} can not always be provided
     * at construction time, because the backing factory may be lazily created.
     */
    @Override
    void initializeHints() {
        super.initializeHints();
        hints.put(Hints.COORDINATE_OPERATION_FACTORY, getBackingFactory());
    }

    /**
     * Returns an operation for conversion or transformation between two coordinate reference
     * systems. If an operation was already created and still in the cache, the cached operation
     * is returned. Otherwise the operation creation is delegated to the
     * {@linkplain CoordinateOperationFactory coordinate operation factory} specified at
     * construction time and the result is cached.
     *
     * @param  sourceCRS Input coordinate reference system.
     * @param  targetCRS Output coordinate reference system.
     * @return A coordinate operation from {@code sourceCRS} to {@code targetCRS}.
     * @throws OperationNotFoundException if no operation path was found from {@code sourceCRS}
     *         to {@code targetCRS}.
     * @throws FactoryException if the operation creation failed for some other reason.
     */
    public CoordinateOperation createOperation(final CoordinateReferenceSystem sourceCRS,
                                               final CoordinateReferenceSystem targetCRS)
            throws OperationNotFoundException, FactoryException
    {
        ensureNonNull("sourceCRS", sourceCRS);
        ensureNonNull("targetCRS", targetCRS);
        final CRSPair key = new CRSPair(sourceCRS, targetCRS);
        CoordinateOperation op;
        synchronized (hints) { // This lock is indirectly required by getBackingFactory().
            op = pool.get(key);
            if (op == null) {
                op = getBackingFactory().createOperation(sourceCRS, targetCRS);
                pool.put(key, op);
            }
        }
        return op;
    }

    /**
     * Returns an operation for conversion or transformation between two coordinate reference
     * systems using the specified method. The current implementation delegates to the
     * {@linkplain CoordinateOperationFactory coordinate operation factory} specified at
     * construction time with no caching.
     *
     * @deprecated Will be removed.
     */
    public CoordinateOperation createOperation(final CoordinateReferenceSystem sourceCRS,
                                               final CoordinateReferenceSystem targetCRS,
                                               final OperationMethod method)
            throws OperationNotFoundException, FactoryException
    {
        synchronized (hints) { // This lock is indirectly required by getBackingFactory().
            return getBackingFactory().createOperation(sourceCRS, targetCRS, method);
        }
    }
}
