/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2005-2006, GeoTools Project Managment Committee (PMC)
 *    (C) 2005, Institut de Recherche pour le Développement
 *   
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation;
 *    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.factory;

// J2SE dependencies and extensions
import java.util.Map;
import java.util.Set;
import java.util.List;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Comparator;
import java.util.LinkedHashSet;
import javax.units.Unit;

// OpenGIS dependencies
import org.opengis.metadata.citation.Citation;
import org.opengis.referencing.IdentifiedObject;
import org.opengis.referencing.AuthorityFactory;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.cs.*;
import org.opengis.referencing.crs.*;
import org.opengis.referencing.datum.*;
import org.opengis.referencing.operation.*;

// Geotools dependencies
import org.geotools.util.CanonicalSet;
import org.geotools.factory.Hints;
import org.geotools.factory.Factory;
import org.geotools.factory.FactoryRegistryException;
import org.geotools.referencing.ReferencingFactoryFinder;
import org.geotools.referencing.AbstractIdentifiedObject;
import org.geotools.referencing.operation.DefiningConversion;
import org.geotools.referencing.cs.DefaultCoordinateSystemAxis;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.Utilities;


/**
 * An authority factory which returns modified {@linkplain CoordinateReferenceSystem CRS},
 * {@linkplain CoordinateSystem CS} or {@linkplain Datum datum} objects from other factory
 * implementations. This class provides a set of {@code replace(...)} methods to be overridden
 * by subclasses in order to replace some {@linkplain CoordinateReferenceSystem CRS},
 * {@linkplain CoordinateSystem CS} or {@linkplain Datum datum} objects by other ones.
 * The replacement rules are determined by the subclass being used. For example the
 * {@link OrderedAxisAuthorityFactory} subclass can replace
 * {@linkplain CoordinateSystem coordinate systems} using (<var>latitude</var>,
 * <var>longitude</var>) axis order by coordinate systems using (<var>longitude</var>,
 * <var>latitude</var>) axis order.
 * <p>
 * All constructors are protected because this class must be subclassed in order to
 * determine which of the {@link DatumAuthorityFactory}, {@link CSAuthorityFactory}
 * and {@link CRSAuthorityFactory} interfaces to implement.
 * 
 * @since 2.3
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/referencing/src/main/java/org/geotools/referencing/factory/TransformedAuthorityFactory.java $
 * @version $Id: TransformedAuthorityFactory.java 25972 2007-06-21 13:38:35Z desruisseaux $
 * @author Martin Desruisseaux
 * 
 * @todo Use generic types for all {@code replace(...)} methods when we will be
 *       allowed to compile for J2SE 1.5, and remove casts in all
 *       {@code createXXX(...)} methods.
 */
public class TransformedAuthorityFactory extends AuthorityFactoryAdapter {
    /**
     * Axis that need to be renamed if their direction changes.
     */
    private static final DefaultCoordinateSystemAxis[] RENAMEABLE = {
        DefaultCoordinateSystemAxis.NORTHING,   DefaultCoordinateSystemAxis.SOUTHING,
        DefaultCoordinateSystemAxis.EASTING,    DefaultCoordinateSystemAxis.WESTING
    };

    /**
     * The coordinate operation factory. Will be created only when first needed.
     */
    private transient CoordinateOperationFactory opFactory;

    /**
     * A pool of modified objects created up to date.
     */
    private final CanonicalSet pool = new CanonicalSet();

    /**
     * Creates a wrapper around the specified factory.
     * 
     * @param factory The factory to wrap.
     */
    protected TransformedAuthorityFactory(final AuthorityFactory factory) {
        super(factory);
    }

    /**
     * Creates a wrapper around the specified factories.
     * 
     * @param crsFactory   The {@linkplain CoordinateReferenceSystem coordinate reference system}
     *                     authority factory, or {@code null}.
     * @param csFactory    The {@linkplain CoordinateSystem coordinate system} authority
     *                     factory, or {@code null}.
     * @param datumFactory The {@linkplain Datum datum} authority factory, or {@code null}.
     * @param opFactory    The {@linkplain CoordinateOperation coordinate operation}
     *                     authority factory, or {@code null}.
     */
    protected TransformedAuthorityFactory(final CRSAuthorityFactory crsFactory,
                                          final CSAuthorityFactory csFactory,
                                          final DatumAuthorityFactory datumFactory,
                                          final CoordinateOperationAuthorityFactory opFactory)
    {
        super(crsFactory, csFactory, datumFactory, opFactory);
    }

    /**
     * Creates a wrappers around the default factories for the specified
     * authority. The factories are fetched using {@link ReferencingFactoryFinder}.
     * 
     * @param authority The authority to wraps (example: {@code "EPSG"}). If {@code null},
     *                  then all authority factories must be explicitly specified in the
     *                  set of hints.
     * @param userHints An optional set of hints, or {@code null} if none.
     * @throws FactoryRegistryException if at least one factory can not be obtained.
     *
     * @since 2.4
     */
    protected TransformedAuthorityFactory(final String authority, final Hints userHints)
            throws FactoryRegistryException
    {
        super(authority, userHints);
    }

    /**
     * Returns the priority for this factory. Priorities are used by
     * {@link ReferencingFactoryFinder} for selecting a preferred factory when many are
     * found for the same service. The default implementation returns
     * <code>{@linkplain #priority priority} + 1</code>, which implies that
     * this adapter has precedence over the wrapped factories. Subclasses should
     * override this method if they want a different priority order for this
     * instance.
     */
    public int getPriority() {
        return priority + 1;
    }

    /**
     * Replaces the specified unit, if applicable. This method is invoked
     * automatically by the {@link #replace(CoordinateSystem)} method. The
     * default implementation returns the unit unchanged.
     * 
     * @param units The units to replace.
     * @return The new units, or {@code units} if no change were needed.
     * @throws FactoryException if an error occured while creating the new units.
     */
    // @Override
    protected Unit replace(final Unit units) throws FactoryException {
        return units;
    }

    /**
     * Replaces the specified direction, if applicable. This method is invoked
     * automatically by the {@link #replace(CoordinateSystem)} method. The
     * default implementation returns the axis direction unchanged.
     * 
     * @param direction The axis direction to replace.
     * @return The new direction, or {@code direction} if no change were needed.
     * @throws FactoryException if an error occured while creating the new axis direction.
     */
    protected AxisDirection replace(final AxisDirection direction) throws FactoryException {
        return direction;
    }

    /**
     * Replaces (if needed) the specified axis by a new one. The default
     * implementation invokes {@link #replace(Unit)} and
     * {@link #replace(AxisDirection)}.
     * 
     * @param axis The coordinate system axis to replace.
     * @return The new coordinate system axis, or {@code axis} if no change were needed.
     * @throws FactoryException if an error occured while creating the new coordinate system axis.
     */
    // @Override
    protected CoordinateSystemAxis replace(CoordinateSystemAxis axis) throws FactoryException {
        final AxisDirection oldDirection = axis.getDirection();
        final AxisDirection newDirection = replace(oldDirection);
              Unit          oldUnits     = axis.getUnit();
        final Unit          newUnits     = replace(oldUnits);
        boolean directionChanged = !oldDirection.equals(newDirection);
        if (directionChanged) {
            /*
             * Check if the direction change implies an axis renaming.  For example if the axis
             * name was "Southing" and the direction has been changed from SOUTH to NORTH, then
             * the axis should be renamed as "Northing".
             */
            final String name = axis.getName().getCode();
            for (int i=0; i<RENAMEABLE.length; i++) {
                if (RENAMEABLE[i].nameMatches(name)) {
                    for (i=0; i<RENAMEABLE.length; i++) {
                        final CoordinateSystemAxis candidate = RENAMEABLE[i];
                        if (newDirection.equals(candidate.getDirection())) {
                            axis = candidate;          // The new axis, but may change again later.
                            oldUnits = axis.getUnit(); // For detecting change relative to new axis.
                            directionChanged = false;  // The new axis has the requested direction.
                            break;
                        }
                    }
                    break;
                }
            }
        }
        if (directionChanged || !oldUnits.equals(newUnits)) {
            final ReferencingFactoryContainer factories = getFactoryContainer(false);
            final CSFactory csFactory = factories.getCSFactory();
            final Map properties = getProperties(axis);
            axis = csFactory.createCoordinateSystemAxis(properties,
                    axis.getAbbreviation(), newDirection, newUnits);
            axis = (CoordinateSystemAxis) pool.unique(axis);
        }
        return axis;
    }

    /**
     * Replaces (if needed) the specified coordinate system by a new one. The
     * default implementation invokes {@link #replace(CoordinateSystemAxis) replace}
     * for each axis. In addition, axis are sorted if this factory implements the
     * {@link Comparator} interface.
     * 
     * @param  cs The coordinate system to replace.
     * @return The new coordinate system, or {@code cs} if no change were needed.
     * @throws FactoryException if an error occured while creating the new coordinate system.
     */
    // @Override
    protected CoordinateSystem replace(final CoordinateSystem cs) throws FactoryException {
        final int dimension = cs.getDimension();
        final CoordinateSystemAxis[] orderedAxis = new CoordinateSystemAxis[dimension];
        for (int i=0; i<dimension; i++) {
            orderedAxis[i] = replace(cs.getAxis(i));
        }
        if (this instanceof Comparator) {
            Arrays.sort(orderedAxis, (Comparator) this);
        }
        for (int i=0; i<dimension; i++) {
            if (!orderedAxis[i].equals(cs.getAxis(i))) {
                CoordinateSystem modified = createCS(cs.getClass(), getProperties(cs), orderedAxis);
                assert Utilities.sameInterfaces(cs.getClass(), modified.getClass(), CoordinateSystem.class);
                modified = (CoordinateSystem) pool.unique(modified);
                return modified;
            }
        }
        // All axis are identical - the CS was actually not changed.
        return cs;
    }

    /**
     * Replaces (if needed) the specified datum by a new one. The default
     * implementation returns the datum unchanged. Subclasses should override
     * this method if some datum replacements are desired.
     * 
     * @param datum The datum to replace.
     * @return The new datum, or {@code datum} if no change were needed.
     * @throws FactoryException if an error occured while creating the new datum.
     */
    // @Override
    protected Datum replace(final Datum datum) throws FactoryException {
        return super.replace(datum);
    }

    /**
     * Replaces (if needed) the specified coordinate reference system. The default
     * implementation checks if there is a {@linkplain #replace(Datum) datum replacement}
     * or a {@linkplain #replace(CoordinateSystem) coordinate system replacement}.
     * If there is at least one of those, then this method returns a new
     * coordinate reference system using the new datum and coordinate system.
     * 
     * @param crs The coordinate reference system to replace.
     * @return A new CRS, or {@code crs} if no change were needed.
     * @throws FactoryException if an error occured while creating the new CRS object.
     */
    // @Override
    protected CoordinateReferenceSystem replace(final CoordinateReferenceSystem crs)
            throws FactoryException
    {
        /*
         * Gets the replaced coordinate system and datum, and checks if there is any change.
         */
        final CoordinateSystem oldCS = crs.getCoordinateSystem();
        final CoordinateSystem cs = replace(oldCS);
        final Datum oldDatum, datum;
        if (crs instanceof SingleCRS) {
            oldDatum = ((SingleCRS) crs).getDatum();
            datum = replace(oldDatum);
        } else {
            datum = oldDatum = null;
        }
        final boolean sameCS = Utilities.equals(cs, oldCS) && Utilities.equals(datum, oldDatum);
        /*
         * Creates a new coordinate reference system using the same properties
         * than the original CRS, except for the coordinate system, datum and
         * authority code.
         */
        CoordinateReferenceSystem modified;
        if (crs instanceof GeneralDerivedCRS) {
            final GeneralDerivedCRS         derivedCRS = (GeneralDerivedCRS) crs;
            final CoordinateReferenceSystem oldBaseCRS = derivedCRS.getBaseCRS();
            final CoordinateReferenceSystem    baseCRS = replace(oldBaseCRS);
            if (sameCS && Utilities.equals(baseCRS, oldBaseCRS)) {
                return crs;
            }
            final Map properties = getProperties(crs);
            final ReferencingFactoryContainer factories = getFactoryContainer(true);
            final CRSFactory crsFactory = factories.getCRSFactory();
            Conversion fromBase = derivedCRS.getConversionFromBase();
            fromBase = new DefiningConversion(getProperties(fromBase),
                    fromBase.getMethod(), fromBase.getParameterValues());
            if (crs instanceof ProjectedCRS) {
                modified = factories.createProjectedCRS(properties,
                        (GeographicCRS) baseCRS, fromBase, (CartesianCS) cs);
            } else {
                // TODO: Need a createDerivedCRS method.
                throw new FactoryException(Errors.format(
                        ErrorKeys.UNSUPPORTED_CRS_$1, crs.getName().getCode()));
            }
        } else if (sameCS) {
            return crs;
        } else {
            final Map properties = getProperties(crs);
            final ReferencingFactoryContainer factories = getFactoryContainer(true);
            final CRSFactory crsFactory = factories.getCRSFactory();
            if (crs instanceof GeographicCRS) {
                modified = crsFactory.createGeographicCRS(properties,
                        (GeodeticDatum) datum, (EllipsoidalCS) cs);
            } else if (crs instanceof GeocentricCRS) {
                final GeodeticDatum gd = (GeodeticDatum) datum;
                if (cs instanceof CartesianCS) {
                    modified = crsFactory.createGeocentricCRS(properties, gd, (CartesianCS) cs);
                } else {
                    modified = crsFactory.createGeocentricCRS(properties, gd, (SphericalCS) cs);
                }
            } else if (crs instanceof VerticalCRS) {
                modified = crsFactory.createVerticalCRS(properties,
                        (VerticalDatum) datum, (VerticalCS) cs);
            } else if (crs instanceof TemporalCRS) {
                modified = crsFactory.createTemporalCRS(properties,
                        (TemporalDatum) datum, (TimeCS) cs);
            } else if (crs instanceof ImageCRS) {
                modified = crsFactory.createImageCRS(properties,
                        (ImageDatum) datum, (AffineCS) cs);
            } else if (crs instanceof EngineeringCRS) {
                modified = crsFactory.createEngineeringCRS(properties,
                        (EngineeringDatum) datum, cs);
            } else if (crs instanceof CompoundCRS) {
                final List/* <CoordinateReferenceSystem> */elements =
                        ((CompoundCRS) crs).getCoordinateReferenceSystems();
                final CoordinateReferenceSystem[] m = new CoordinateReferenceSystem[elements.size()];
                for (int i=0; i<m.length; i++) {
                    m[i] = replace((CoordinateReferenceSystem) elements.get(i));
                }
                modified = crsFactory.createCompoundCRS(properties, m);
            } else {
                throw new FactoryException(Errors.format(
                        ErrorKeys.UNSUPPORTED_CRS_$1, crs.getName().getCode()));
            }
        }
        modified = (CoordinateReferenceSystem) pool.unique(modified);
        return modified;
    }

    /**
     * Replaces (if needed) the specified coordinate operation. The default
     * implementation checks if there is a source or target
     * {@linkplain #replace(CoordinateReferenceSystem) CRS replacement}. If
     * there is at least one of those, then this method returns a new coordinate
     * operation using the new CRS.
     * 
     * @param operation The coordinate operation to replace.
     * @return A new operation, or {@code operation} if no change were needed.
     * @throws FactoryException if an error occured while creating the new operation object.
     */
    // @Override
    protected CoordinateOperation replace(final CoordinateOperation operation)
            throws FactoryException
    {
        final CoordinateReferenceSystem oldSrcCRS = operation.getSourceCRS();
        final CoordinateReferenceSystem oldTgtCRS = operation.getTargetCRS();
        final CoordinateReferenceSystem sourceCRS = (oldSrcCRS != null) ? replace(oldSrcCRS) : null;
        final CoordinateReferenceSystem targetCRS = (oldTgtCRS != null) ? replace(oldTgtCRS) : null;
        if (Utilities.equals(oldSrcCRS, sourceCRS) && Utilities.equals(oldTgtCRS, targetCRS)) {
            return operation;
        }
        if (opFactory == null) {
            opFactory = getCoordinateOperationFactory();
        }
        CoordinateOperation modified;
        modified = opFactory.createOperation(sourceCRS, targetCRS);
        modified = (CoordinateOperation) pool.unique(modified);
        return modified;
    }

    /**
     * Creates a new coordinate system of the specified kind. This method is
     * invoked automatically by {@link #replace(CoordinateSystem)} after it
     * determined that the axis need to be changed.
     * 
     * @param type       The coordinate system type to create.
     * @param properties The properties to gives to the new coordinate system.
     * @param axis       The axis to give to the new coordinate system. Subclasses are
     *                   allowed to write directly in this array (no need to copy it).
     * @return A new coordinate system of the specified kind with the specified axis.
     * @throws FactoryException if the coordinate system can't be created.
     */
    private CoordinateSystem createCS(final Class/* <CoordinateSystem> */type,
                                      final Map properties,
                                      final CoordinateSystemAxis[] axis)
            throws FactoryException
    {
        final int dimension = axis.length;
        final ReferencingFactoryContainer factories = getFactoryContainer(false);
        final CSFactory csFactory = factories.getCSFactory();
        if (CartesianCS.class.isAssignableFrom(type)) {
            switch (dimension) {
                case 2: return csFactory.createCartesianCS(properties, axis[0], axis[1]);
                case 3: return csFactory.createCartesianCS(properties, axis[0], axis[1], axis[2]);
            }
        } else if (EllipsoidalCS.class.isAssignableFrom(type)) {
            switch (dimension) {
                case 2: return csFactory.createEllipsoidalCS(properties, axis[0], axis[1]);
                case 3: return csFactory.createEllipsoidalCS(properties, axis[0], axis[1], axis[2]);
            }
        } else if (SphericalCS.class.isAssignableFrom(type)) {
            switch (dimension) {
                case 3: return csFactory.createSphericalCS(properties, axis[0], axis[1], axis[2]);
            }
        } else if (CylindricalCS.class.isAssignableFrom(type)) {
            switch (dimension) {
                case 3: return csFactory.createCylindricalCS(properties, axis[0], axis[1], axis[2]);
            }
        } else if (PolarCS.class.isAssignableFrom(type)) {
            switch (dimension) {
                case 2: return csFactory.createPolarCS(properties, axis[0], axis[1]);
            }
        } else if (VerticalCS.class.isAssignableFrom(type)) {
            switch (dimension) {
                case 1: return csFactory.createVerticalCS(properties, axis[0]);
            }
        } else if (TimeCS.class.isAssignableFrom(type)) {
            switch (dimension) {
                case 1: return csFactory.createTimeCS(properties, axis[0]);
            }
        } else if (LinearCS.class.isAssignableFrom(type)) {
            switch (dimension) {
                case 1: return csFactory.createLinearCS(properties, axis[0]);
            }
        } else if (UserDefinedCS.class.isAssignableFrom(type)) {
            switch (dimension) {
                case 2: return csFactory.createUserDefinedCS(properties, axis[0], axis[1]);
                case 3: return csFactory.createUserDefinedCS(properties, axis[0], axis[1], axis[2]);
            }
        }
        throw new FactoryException(Errors.format(ErrorKeys.UNSUPPORTED_COORDINATE_SYSTEM_$1,
                                   Utilities.getShortName(type)));
    }

    /**
     * Returns the properties to be given to an object replacing an original
     * one. If the new object keep the same authority, then all metadata are
     * preserved. Otherwise (i.e. if a new authority is given to the new
     * object), then the old identifiers will be removed from the new object
     * metadata.
     * 
     * @param object The original object.
     * @return The properties to be given to the object created as a substitute
     *         of {@code object}.
     */
    private Map getProperties(final IdentifiedObject object) {
        final Citation authority = getAuthority();
        if (!Utilities.equals(authority, object.getName().getAuthority())) {
            return AbstractIdentifiedObject.getProperties(object, authority);
        } else {
            return AbstractIdentifiedObject.getProperties(object);
        }
    }

    /**
     * Creates an operation from coordinate reference system codes. The default
     * implementation first invokes the same method from the
     * {@linkplain #operationFactory underlying operation factory}, and next
     * invokes {@link #replace(CoordinateOperation) replace} for each
     * operations.
     */
    public Set/* <CoordinateOperation> */createFromCoordinateReferenceSystemCodes(
            final String sourceCode, final String targetCode)
            throws FactoryException
    {
        final Set/* <CoordinateOperation> */operations, modified;
        operations = super.createFromCoordinateReferenceSystemCodes(sourceCode, targetCode);
        modified = new LinkedHashSet((int) (operations.size() / 0.75f) + 1);
        for (final Iterator it = operations.iterator(); it.hasNext();) {
            final CoordinateOperation operation;
            try {
                operation = (CoordinateOperation) it.next();
            } catch (BackingStoreException exception) {
                final Throwable cause = exception.getCause();
                if (cause instanceof FactoryException) {
                    throw (FactoryException) cause;
                } else {
                    throw exception;
                }
            }
            modified.add(replace(operation));
        }
        return modified;
    }

    /**
     * Releases resources immediately instead of waiting for the garbage
     * collector. This method do <strong>not</strong> dispose the resources of
     * wrapped factories (e.g. {@link #crsFactory crsFactory}), because they may
     * still in use by other classes.
     */
    public synchronized void dispose() throws FactoryException {
        pool.clear();
        super.dispose();
    }
}
