/*
 *    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.Arrays;
import java.util.HashMap;
import java.util.Comparator;
import javax.units.Unit;
import javax.units.SI;
import javax.units.NonSI;

// OpenGIS dependencies
import org.opengis.referencing.cs.*;
import org.opengis.referencing.crs.CRSAuthorityFactory;
import org.opengis.referencing.FactoryException;

// Geotools dependencies
import org.geotools.factory.Hints;
import org.geotools.factory.FactoryRegistryException;
import org.geotools.referencing.ReferencingFactoryFinder;
import org.geotools.referencing.cs.DefaultCoordinateSystemAxis;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.i18n.ErrorKeys;


/**
 * An authority factory which delegates all the work to an other factory, and reorder the axis in
 * some pre-determined order. This factory is mostly used by application expecting geographic
 * coordinates in (<var>longitude</var>, <var>latitude</var>) order, while most geographic CRS
 * specified in the <A HREF="http://www.epsg.org">EPSG database</A> use the opposite axis order.
 * <p>
 * It is better to avoid this class if you can. This class exists primarily for compatibility with
 * external data or applications that assume (<var>longitude</var>, <var>latitude</var>) axis order
 * no matter what the EPSG database said, for example Shapefiles.
 * <p>
 * The axis order can be specified at construction time as an array of {@linkplain AxisDirection
 * axis directions}. If no such array is explicitly specified, then the default order is
 * {@linkplain AxisDirection#EAST               East},
 * {@linkplain AxisDirection#EAST_NORTH_EAST    East-North-East},
 * {@linkplain AxisDirection#NORTH_EAST         North-East},
 * {@linkplain AxisDirection#NORTH_NORTH_EAST   North-North-East},
 * {@linkplain AxisDirection#NORTH              North},
 * {@linkplain AxisDirection#UP                 Up},
 * {@linkplain AxisDirection#GEOCENTRIC_X       Geocentric X},
 * {@linkplain AxisDirection#GEOCENTRIC_Y       Geocentric Y},
 * {@linkplain AxisDirection#GEOCENTRIC_Z       Geocentric Z},
 * {@linkplain AxisDirection#COLUMN_POSITIVE    Column},
 * {@linkplain AxisDirection#ROW_POSITIVE       Row},
 * {@linkplain AxisDirection#DISPLAY_RIGHT      Display right},
 * {@linkplain AxisDirection#DISPLAY_UP         Display up} and
 * {@linkplain AxisDirection#FUTURE             Future}.
 * This means that, for example, axis with East or West direction will be placed before any
 * axis with North or South direction. Axis directions not specified in the table (for example
 * {@link AxisDirection#OTHER OTHER}) will be ordered last. This is somewhat equivalent to the
 * ordering of {@link Double#NaN NaN} values in an array of {@code double}.
 * <p>
 * <strong>Notes:</strong>
 * <ul>
 *   <li>This class compares only the "{@linkplain AxisDirection#absolute absolute}" axis
 *       directions, so North and South are considered equivalent.</li>
 *   <li>The default direction order may changes in future Geotools version in order
 *       to fit what appears to be the most common usage on the market.</li>
 *   <li>The actual axis ordering is determined by the {@link #compare compare} method
 *       implementation. Subclasses may override this method if the want to provide a more
 *       sophesticated axis ordering.</li>
 * </ul>
 * <p>
 * For some authority factories, an instance of this class can be obtained by passing a
 * {@link Hints#FORCE_LONGITUDE_FIRST_AXIS_ORDER FORCE_LONGITUDE_FIRST_AXIS_ORDER} hint
 * to the <code>{@linkplain ReferencingFactoryFinder#getCRSAuthorityFactory
 * FactoryFinder.getCRSAuthorityFactory}(...)</code> method. Whatever this hint is supported
 * or not is authority dependent. Example:
 *
 * <blockquote><pre>
 * Hints                   hints = new Hints(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER, Boolean.TRUE);
 * CRSAuthorityFactory   factory = FactoryFinder.getCRSAuthorityFactory("EPSG", hints);
 * CoordinateReferenceSystem crs = factory.createCoordinateReferenceSystem("EPSG:4326");
 * </pre></blockquote>
 *
 * This class is named <cite>ordered axis authority factory</cite> instead of something like
 * <cite>longitude first axis order</cite> because the axis order can be user-supplied. The
 * (<var>longitude</var>, <var>latitude</var>) order just appears to be the default one.
 *
 * @since 2.2
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/referencing/src/main/java/org/geotools/referencing/factory/OrderedAxisAuthorityFactory.java $
 * @version $Id: OrderedAxisAuthorityFactory.java 25406 2007-05-03 18:08:10Z desruisseaux $
 * @author Martin Desruisseaux
 *
 * @see Hints#FORCE_LONGITUDE_FIRST_AXIS_ORDER
 * @see Hints#FORCE_STANDARD_AXIS_UNITS
 * @tutorial http://docs.codehaus.org/display/GEOTOOLS/The+axis+order+issue
 */
public class OrderedAxisAuthorityFactory extends TransformedAuthorityFactory
        implements CSAuthorityFactory, CRSAuthorityFactory, Comparator/*<CoordinateSystemAxis>*/
{
    /**
     * The default order for axis directions. Note that this array needs to contain only the
     * "{@linkplain AxisDirection#absolute absolute}" directions.
     *
     * REMINDER: If this array is modified, don't forget to update the class javadoc above.
     */
    private static final AxisDirection[] DEFAULT_ORDER = {
        AxisDirection.EAST,
        AxisDirection.EAST_NORTH_EAST,
        AxisDirection.NORTH_EAST,
        AxisDirection.NORTH_NORTH_EAST,
        AxisDirection.NORTH,
        AxisDirection.UP,
        AxisDirection.GEOCENTRIC_X,
        AxisDirection.GEOCENTRIC_Y,
        AxisDirection.GEOCENTRIC_Z,
        AxisDirection.COLUMN_POSITIVE,
        AxisDirection.ROW_POSITIVE,
        AxisDirection.DISPLAY_RIGHT,
        AxisDirection.DISPLAY_UP,
        AxisDirection.FUTURE
    };

    /**
     * The rank to be given to each axis direction. The rank is stored at the indice
     * corresponding to the direction {@linkplain AxisDirection#ordinal ordinal} value.
     */
    private final int[] directionRanks;

    /**
     * {@code true} if this authority factory should also force the axis to their standard
     * direction. For example if {@code true}, then axis with increasing values toward South
     * will be converted to axis with increasing values toward North. The default value is
     * {@code false}.
     *
     * @see Hints#FORCE_STANDARD_AXIS_DIRECTIONS
     * @since 2.3
     */
    protected final boolean forceStandardDirections;

    /**
     * {@code true} if this authority factory should also force all angular units to
     * decimal degrees and linear units to meters. The default value is {@code false}.
     *
     * @see Hints#FORCE_STANDARD_AXIS_UNITS
     * @since 2.3
     */
    protected final boolean forceStandardUnits;

    /**
     * Creates a factory which will reorder the axis of all objects created by the default
     * authority factories. The factories are fetched using {@link ReferencingFactoryFinder}. This
     * constructor accepts the following hints:
     * <p>
     * <ul>
     *   <li>{@link Hints#FORCE_STANDARD_AXIS_UNITS}</li>
     *   <li>{@link Hints#FORCE_STANDARD_AXIS_DIRECTIONS}</li>
     *   <li>All hints understood by {@link ReferencingFactoryFinder}</li>
     * </ul>
     *
     * @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.
     * @param  axisOrder An array of axis directions that determine the axis order wanted,
     *         or {@code null} for the default axis order.
     * @throws FactoryRegistryException if at least one factory can not be obtained.
     * @throws IllegalArgumentException If at least two axis directions are colinear.
     *
     * @since 2.3
     */
    public OrderedAxisAuthorityFactory(final String          authority,
                                       final Hints           userHints,
                                       final AxisDirection[] axisOrder)
            throws FactoryRegistryException, IllegalArgumentException
    {
        super(authority, userHints);
        forceStandardUnits      = booleanValue(userHints, Hints.FORCE_STANDARD_AXIS_UNITS);
        forceStandardDirections = booleanValue(userHints, Hints.FORCE_STANDARD_AXIS_DIRECTIONS);
        directionRanks          = computeDirectionRanks(axisOrder);
        completeHints();
    }

    /**
     * Creates a factory which will reorder the axis of all objects created by the supplied
     * factory. This constructor accepts the following optional hints:
     * <p>
     * <ul>
     *   <li>{@link Hints#FORCE_STANDARD_AXIS_UNITS}</li>
     *   <li>{@link Hints#FORCE_STANDARD_AXIS_DIRECTIONS}</li>
     * </ul>
     *
     * @param  factory   The factory that produces objects using arbitrary axis order.
     * @param  userHints An optional set of hints, or {@code null} if none.
     * @param  axisOrder An array of axis directions that determine the axis order wanted,
     *                   or {@code null} for the default axis order.
     * @throws IllegalArgumentException If at least two axis directions are colinear.
     *
     * @since 2.3
     */
    public OrderedAxisAuthorityFactory(final AbstractAuthorityFactory factory,
                                       final Hints                    userHints,
                                       final AxisDirection[]          axisOrder)
            throws IllegalArgumentException
    {
        super(factory);
        forceStandardUnits      = booleanValue(userHints, Hints.FORCE_STANDARD_AXIS_UNITS);
        forceStandardDirections = booleanValue(userHints, Hints.FORCE_STANDARD_AXIS_DIRECTIONS);
        directionRanks          = computeDirectionRanks(axisOrder);
        completeHints();
    }

    /**
     * Returns the boolean value for the specified hint.
     */
    private static boolean booleanValue(final Hints userHints, final Hints.Key key) {
        if (userHints != null) {
            final Boolean value = (Boolean) userHints.get(key);
            if (value != null) {
                return value.booleanValue();
            }
        }
        return false;
    }

    /**
     * Completes the set of hints according the value currently set in this object.
     * This method is invoked by constructors only.
     */
    private void completeHints() {
        hints.put(Hints.FORCE_STANDARD_AXIS_UNITS,      Boolean.valueOf(forceStandardUnits));
        hints.put(Hints.FORCE_STANDARD_AXIS_DIRECTIONS, Boolean.valueOf(forceStandardDirections));
        // The following hint has no effect on this class behaviour,
        // but tells to the user what this factory do about axis order.
        if (compare(DefaultCoordinateSystemAxis.EASTING, DefaultCoordinateSystemAxis.NORTHING) < 0) {
            hints.put(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER, Boolean.TRUE);
        }
    }

    /**
     * Computes the rank for every direction in the specified. The rank is stored in an array
     * at the indice corresponding to the direction {@linkplain AxisDirection#ordinal ordinal}
     * value. This method is used by constructors for computing the {@link #directionRanks} field.
     *
     * @throws IllegalArgumentException If at least two axis directions are colinear.
     */
    private static int[] computeDirectionRanks(AxisDirection[] axisOrder)
            throws IllegalArgumentException
    {
        if (axisOrder == null) {
            axisOrder = DEFAULT_ORDER;
        }
        int length = 0;
        for (int i=0; i<axisOrder.length; i++) {
            final int ordinal = axisOrder[i].absolute().ordinal() + 1;
            if (ordinal > length) {
                length = ordinal;
            }
        }
        final int[] directionRanks = new int[length];
        Arrays.fill(directionRanks, length);
        for (int i=0; i<axisOrder.length; i++) {
            final int ordinal  = axisOrder[i].absolute().ordinal();
            final int previous = directionRanks[ordinal];
            if (previous != length) {
                // TODO: Use the localized version of 'getName' in GeoAPI 2.1
                throw new IllegalArgumentException(Errors.format(ErrorKeys.COLINEAR_AXIS_$2,
                                          axisOrder[previous].name(), axisOrder[i].name()));
            }
            directionRanks[ordinal] = i;
        }
        return directionRanks;
    }

    /**
     * Returns the rank for the specified axis. Any axis that were not specified
     * at construction time will ordered after all known axis.
     */
    private final int rank(final CoordinateSystemAxis axis) {
        int c = axis.getDirection().absolute().ordinal();
        c = (c>=0 && c<directionRanks.length) ? directionRanks[c] : directionRanks.length;
        return c;
    }

    /**
     * Compares two axis for order. This method is invoked automatically by the
     * {@link #replace(CoordinateSystem) replace} method for ordering the axis in a
     * coordinate system. The default implementation orders the axis according their
     * {@linkplain CoordinateSystemAxis#getDirection direction}, using the direction
     * table given at {@linkplain #OrderedAxisAuthorityFactory(AbstractAuthorityFactory,
     * Hints, AxisDirection[]) construction time} (see also the class description).
     * Subclasses may override this method if they want to define a more sophesticated
     * axis ordering.
     *
     * @param  axis1 The first axis to compare.
     * @param  axis2 The second axis to compare.
     * @return A negative integer if {@code axis1} should appears before {@code axis2}, or a
     *         positive number if {@code axis2} should appears before {@code axis1}, or 0 if
     *         the two axis are unordered one relative to the other.
     *
     * @todo The argument type will be changed to {@link CoordinateSystemAxis} when we will
     *       be allowed to compile for J2SE 1.5.
     *
     * @since 2.3
     */
    public int compare(final Object axis1, final Object axis2) {
        return rank((CoordinateSystemAxis) axis1) - rank((CoordinateSystemAxis) axis2);
    }

    /**
     * Replaces the specified unit, if applicable. This method is invoked automatically by the
     * {@link #replace(CoordinateSystem)} method. The default implementation replaces the unit
     * only if the {@link Hints#FORCE_STANDARD_AXIS_UNITS FORCE_STANDARD_AXIS_UNITS} hint was
     * specified as {@link Boolean#TRUE TRUE} at construction time. In such case, the default
     * substitution table is:
     * <p>
     * <ul>
     *   <li>Any linear units converted to {@linkplain SI#METER meters}</li>
     *   <li>{@linkplain SI#RADIAN Radians} and {@linkplain NonSI#GRADE grades} converted to
     *       {@linkplain NonSI#DEGREE_ANGLE decimal degrees}</li>
     * </ul>
     * <p>
     * This default substitution table may be expanded in future Geotools versions.
     *
     * @since 2.3
     */
    protected Unit replace(final Unit units) {
        if (forceStandardUnits) {
            if (units.isCompatible(SI.METER)) {
                return SI.METER;
            }
            if (units.equals(SI.RADIAN) || units.equals(NonSI.GRADE)) {
                return NonSI.DEGREE_ANGLE;
            }
        }
        return units;
    }

    /**
     * Replaces the specified direction, if applicable. This method is invoked automatically by the
     * {@link #replace(CoordinateSystem)} method. The default implementation replaces the direction
     * only if the {@link Hints#FORCE_STANDARD_AXIS_DIRECTIONS FORCE_STANDARD_AXIS_DIRECTIONS} hint
     * was specified as {@link Boolean#TRUE TRUE} at construction time. In such case, the default
     * substitution table is as specified in the {@link AxisDirection#absolute} method.
     * Subclasses may override this method if they want to use a different substitution table.
     *
     * @since 2.3
     */
    protected AxisDirection replace(final AxisDirection direction) {
        return (forceStandardDirections) ? direction.absolute() : direction;
    }

    /**
     * Returns the error message for the specified coordinate system.
     * Used when throwing {@link FactoryException}.
     */
    private static final String getErrorMessage(final CoordinateSystem cs) {
        return Errors.format(ErrorKeys.UNSUPPORTED_COORDINATE_SYSTEM_$1, cs.getName().getCode());
    }
}
