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

import java.util.List;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Map;
import javax.measure.unit.SI;
import javax.measure.unit.Unit;

import org.opengis.referencing.cs.*;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.i18n.ErrorKeys;


/**
 * Converts an arbitrary CS into one of the predefined constants provided in the
 * {@link org.geotools.referencing.cs} package. The main usage for this class is
 * to reorder the axis in some "standard" order like (<var>x</var>, <var>y</var>,
 * <var>z</var>) or (<var>longitude</var>, <var>latitude</var>). What "standard"
 * order means is sometime an arbitrary choice, which explain why this class is
 * not public at this time.
 *
 * @source $URL: http://svn.osgeo.org/geotools/branches/2.7.x/modules/library/referencing/src/main/java/org/geotools/referencing/cs/PredefinedCS.java $
 * @version $Id: PredefinedCS.java 30760 2008-06-18 14:28:24Z desruisseaux $
 * @author Martin Desruisseaux (IRD)
 */
final class PredefinedCS implements Comparator<CoordinateSystem> {
    /**
     * An instance of {@link PredefinedCS}. Will be created only when first needed.
     */
    private static Comparator<CoordinateSystem> csComparator;

    /**
     * Our ordering for coordinate system objects.
     */
    @SuppressWarnings("unchecked")
    private final Class<? extends CoordinateSystem>[] types = new Class[] {
        CartesianCS  .class,
        AffineCS     .class,
        EllipsoidalCS.class,
        SphericalCS  .class,
        CylindricalCS.class,
        PolarCS      .class,
        VerticalCS   .class,
        TimeCS       .class,
        LinearCS     .class,
        UserDefinedCS.class
    };

    /**
     * Creates a comparator.
     */
    private PredefinedCS() {
    }

    /**
     * Compares the ordering between two coordinate systems. This comparator is used for sorting
     * the axis in an user-supplied compound CS in an order closes to some "standard" order.
     */
    public int compare(final CoordinateSystem object1, final CoordinateSystem object2) {
        final Class<? extends CoordinateSystem> type1 = object1.getClass();
        final Class<? extends CoordinateSystem> type2 = object2.getClass();
        for (int i=0; i<types.length; i++) {
            final Class<?> type = types[i];
            final boolean a1 = type.isAssignableFrom(type1);
            final boolean a2 = type.isAssignableFrom(type2);
            if (a1) return a2 ? 0 : -1;
            if (a2) return a1 ? 0 : +1;
        }
        return 0;
    }

    /**
     * Implementation of the {@link AbstractCS#standard} method.
     */
    static CoordinateSystem standard(final CoordinateSystem cs) throws IllegalArgumentException {
        final int dimension = cs.getDimension();
        if (cs instanceof CartesianCS) {
            switch (dimension) {
                case 2: {
                    if (DefaultCartesianCS.PROJECTED.axisColinearWith(cs)) {
                        return DefaultCartesianCS.PROJECTED;
                    }
                    if (DefaultCartesianCS.GRID.axisColinearWith(cs)) {
                        return DefaultCartesianCS.GRID;
                    }
                    if (DefaultCartesianCS.GENERIC_2D.directionColinearWith(cs)) {
                        return DefaultCartesianCS.GENERIC_2D;
                    }
                    return rightHanded((CartesianCS) cs);
                }
                case 3: {
                    if (DefaultCartesianCS.GEOCENTRIC.axisColinearWith(cs)) {
                        return DefaultCartesianCS.GEOCENTRIC;
                    }
                    if (DefaultCartesianCS.GENERIC_3D.directionColinearWith(cs)) {
                        return DefaultCartesianCS.GENERIC_3D;
                    }
                    return rightHanded((CartesianCS) cs);
                }
            }
        }
        if (cs instanceof AffineCS) {
            return rightHanded((AffineCS) cs);
        }
        if (cs instanceof EllipsoidalCS) {
            switch (dimension) {
                case 2: return DefaultEllipsoidalCS.GEODETIC_2D;
                case 3: return DefaultEllipsoidalCS.GEODETIC_3D;
            }
        }
        if (cs instanceof SphericalCS) {
            switch (dimension) {
                case 3: return DefaultSphericalCS.GEOCENTRIC;
            }
        }
        if (cs instanceof VerticalCS) {
            switch (dimension) {
                case 1: {
                    return DefaultVerticalCS.ELLIPSOIDAL_HEIGHT;
                }
            }
        }
        if (cs instanceof TimeCS) {
            switch (dimension) {
                case 1: return DefaultTimeCS.DAYS;
            }
        }
        if (cs instanceof DefaultCompoundCS) {
            final List<CoordinateSystem> components = ((DefaultCompoundCS) cs).getCoordinateSystems();
            final CoordinateSystem[] user = new CoordinateSystem[components.size()];
            final CoordinateSystem[] std  = new CoordinateSystem[user.length];
            for (int i=0; i<std.length; i++) {
                std[i] = standard(user[i] = components.get(i));
            }
            if (csComparator == null) {
                csComparator = new PredefinedCS();
            }
            Arrays.sort(std, csComparator);
            return Arrays.equals(user, std) ? cs : new DefaultCompoundCS(std);
        }
        throw new IllegalArgumentException(
                Errors.format(ErrorKeys.UNSUPPORTED_COORDINATE_SYSTEM_$1, cs.getName().getCode()));
    }

    /**
     * Reorder the axis in the specified Affine CS in an attempt to get a right-handed system.
     * Units are standardized to meters in the process. If no axis change is needed, then this
     * method returns {@code cs} unchanged.
     */
    private static AffineCS rightHanded(final AffineCS cs) {
        boolean changed = false;
        final int dimension = cs.getDimension();
        final CoordinateSystemAxis[] axis = new CoordinateSystemAxis[dimension];
        for (int i=0; i<dimension; i++) {
            /*
             * Gets the axis and replaces it by one of the predefined constants declared in
             * DefaultCoordinateSystemAxis, if possible. The predefined constants use ISO 19111
             * names with metres or degrees units, so it is pretty close to the "standard" axis
             * we are looking for.
             */
            CoordinateSystemAxis axe = axis[i] = cs.getAxis(i);
            DefaultCoordinateSystemAxis standard = DefaultCoordinateSystemAxis.getPredefined(axe);
            if (standard != null) {
                axe = standard;
            }
            /*
             * Changes units to meters. Every units in an affine CS should be linear or
             * dimensionless (the later is used for grid coordinates).  The 'usingUnit'
             * method will thrown an exception if the unit is incompatible. See
             * DefaultAffineCS.isCompatibleUnit(Unit).
             */
            final Unit<?> unit = axe.getUnit();
            if (!Unit.ONE.equals(unit) && !SI.METER.equals(unit)) {
                if (!(axe instanceof DefaultCoordinateSystemAxis)) {
                    axe = new DefaultCoordinateSystemAxis(axe);
                }
                axe = ((DefaultCoordinateSystemAxis) axe).usingUnit(SI.METER);
            }
            changed |= (axe != axis[i]);
            axis[i] = axe;
        }
        /*
         * Sorts the axis in an attempt to create a right-handed system
         * and creates a new Coordinate System if at least one axis changed.
         */
        changed |= ComparableAxisWrapper.sort(axis);
        if (!changed) {
            return cs;
        }
        final Map<String,?> properties = DefaultAffineCS.getProperties(cs, null);
        if (cs instanceof CartesianCS) {
            return new DefaultCartesianCS(properties, axis);
        }
        return new DefaultAffineCS(properties, axis);
    }
}
