/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.sis.referencing.factory;

import java.util.Map;
import java.util.Set;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Collections;
import java.util.Arrays;
import javax.measure.Unit;
import javax.measure.quantity.Length;
import org.opengis.util.FactoryException;
import org.opengis.util.InternationalString;
import org.opengis.metadata.citation.Citation;
import org.opengis.referencing.IdentifiedObject;
import org.opengis.referencing.NoSuchAuthorityCodeException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.crs.CRSFactory;
import org.opengis.referencing.crs.CRSAuthorityFactory;
import org.opengis.referencing.crs.EngineeringCRS;
import org.opengis.referencing.crs.GeographicCRS;
import org.opengis.referencing.crs.ProjectedCRS;
import org.opengis.referencing.crs.VerticalCRS;
import org.opengis.referencing.crs.SingleCRS;
import org.opengis.referencing.cs.CSFactory;
import org.opengis.referencing.cs.CartesianCS;
import org.opengis.referencing.cs.AxisDirection;
import org.opengis.referencing.datum.DatumFactory;
import org.opengis.referencing.datum.EngineeringDatum;
import org.apache.sis.internal.referencing.provider.TransverseMercator.Zoner;
import org.apache.sis.internal.referencing.GeodeticObjectBuilder;
import org.apache.sis.internal.referencing.Resources;
import org.apache.sis.metadata.iso.citation.Citations;
import org.apache.sis.internal.system.DefaultFactories;
import org.apache.sis.internal.system.Loggers;
import org.apache.sis.internal.util.Constants;
import org.apache.sis.measure.Units;
import org.apache.sis.referencing.CommonCRS;
import org.apache.sis.referencing.cs.CoordinateSystems;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.iso.SimpleInternationalString;


/**
 * Creates coordinate reference systems in the "{@code OGC}", "{@code CRS}" or {@code "AUTO(2)"} namespaces.
 * All namespaces recognized by this factory are defined by the Open Geospatial Consortium (OGC).
 * Most codes map to one of the constants in the {@link CommonCRS} enumeration.
 *
 * <table class="sis">
 *   <caption>Recognized Coordinate Reference System codes</caption>
 *   <tr>
 *     <th>Namespace</th>
 *     <th>Code</th>
 *     <th>Name</th>
 *     <th>Datum type</th>
 *     <th>CS type</th>
 *     <th>Axis direction</th>
 *     <th>Units</th>
 *   </tr><tr>
 *     <td>CRS</td>
 *     <td>1</td>
 *     <td>Computer display</td>
 *     <td>{@linkplain org.apache.sis.referencing.crs.DefaultEngineeringCRS Engineering}</td>
 *     <td>{@linkplain org.apache.sis.referencing.cs.DefaultCartesianCS Cartesian}</td>
 *     <td>(east, south)</td>
 *     <td>pixels</td>
 *   </tr><tr>
 *     <td>CRS</td>
 *     <td>27</td>
 *     <td>NAD27</td>
 *     <td>{@linkplain org.apache.sis.referencing.crs.DefaultGeographicCRS Geographic}</td>
 *     <td>{@linkplain org.apache.sis.referencing.cs.DefaultEllipsoidalCS Ellipsoidal}</td>
 *     <td>(east, north)</td>
 *     <td>degrees</td>
 *   </tr><tr>
 *     <td>CRS</td>
 *     <td>83</td>
 *     <td>NAD83</td>
 *     <td>{@linkplain org.apache.sis.referencing.crs.DefaultGeographicCRS Geographic}</td>
 *     <td>{@linkplain org.apache.sis.referencing.cs.DefaultEllipsoidalCS Ellipsoidal}</td>
 *     <td>(east, north)</td>
 *     <td>degrees</td>
 *   </tr><tr>
 *     <td>CRS</td>
 *     <td>84</td>
 *     <td>WGS84</td>
 *     <td>{@linkplain org.apache.sis.referencing.crs.DefaultGeographicCRS Geographic}</td>
 *     <td>{@linkplain org.apache.sis.referencing.cs.DefaultEllipsoidalCS Ellipsoidal}</td>
 *     <td>(east, north)</td>
 *     <td>degrees</td>
 *   </tr><tr>
 *     <td>CRS</td>
 *     <td>88</td>
 *     <td>NAVD88</td>
 *     <td>{@linkplain org.apache.sis.referencing.crs.DefaultVerticalCRS Vertical}</td>
 *     <td>{@linkplain org.apache.sis.referencing.cs.DefaultVerticalCS Vertical}</td>
 *     <td>up</td>
 *     <td>metres</td>
 *   </tr><tr>
 *     <td>AUTO2</td>
 *     <td>42001</td>
 *     <td>WGS 84 / Auto UTM</td>
 *     <td>{@linkplain org.apache.sis.referencing.crs.DefaultProjectedCRS Projected}</td>
 *     <td>{@linkplain org.apache.sis.referencing.cs.DefaultCartesianCS Cartesian}</td>
 *     <td>(east, north)</td>
 *     <td>user-specified</td>
 *   </tr><tr>
 *     <td>AUTO2</td>
 *     <td>42002</td>
 *     <td>WGS 84 / Auto Tr Mercator</td>
 *     <td>{@linkplain org.apache.sis.referencing.crs.DefaultProjectedCRS Projected}</td>
 *     <td>{@linkplain org.apache.sis.referencing.cs.DefaultCartesianCS Cartesian}</td>
 *     <td>(east, north)</td>
 *     <td>user-specified</td>
 *   </tr><tr>
 *     <td>AUTO2</td>
 *     <td>42003</td>
 *     <td>WGS 84 / Auto Orthographic</td>
 *     <td>{@linkplain org.apache.sis.referencing.crs.DefaultProjectedCRS Projected}</td>
 *     <td>{@linkplain org.apache.sis.referencing.cs.DefaultCartesianCS Cartesian}</td>
 *     <td>(east, north)</td>
 *     <td>user-specified</td>
 *   </tr><tr>
 *     <td>AUTO2</td>
 *     <td>42004</td>
 *     <td>WGS 84 / Auto Equirectangular</td>
 *     <td>{@linkplain org.apache.sis.referencing.crs.DefaultProjectedCRS Projected}</td>
 *     <td>{@linkplain org.apache.sis.referencing.cs.DefaultCartesianCS Cartesian}</td>
 *     <td>(east, north)</td>
 *     <td>user-specified</td>
 *   </tr><tr>
 *     <td>AUTO2</td>
 *     <td>42005</td>
 *     <td>WGS 84 / Auto Mollweide</td>
 *     <td>{@linkplain org.apache.sis.referencing.crs.DefaultProjectedCRS Projected}</td>
 *     <td>{@linkplain org.apache.sis.referencing.cs.DefaultCartesianCS Cartesian}</td>
 *     <td>(east, north)</td>
 *     <td>user-specified</td>
 *   </tr>
 * </table>
 *
 * <div class="section">Note on codes in CRS namespace</div>
 * The format is usually "{@code CRS:}<var>n</var>" where <var>n</var> is a number like 27, 83 or 84.
 * However this factory is lenient and allows the {@code CRS} part to be repeated as in {@code "CRS:CRS84"}.
 * It also accepts {@code "OGC"} as a synonymous of the {@code "CRS"} namespace.
 *
 * <div class="note"><b>Examples:</b>
 * {@code "CRS:27"}, {@code "CRS:83"}, {@code "CRS:84"}, {@code "CRS:CRS84"}, {@code "OGC:CRS84"}.</div>
 *
 * <div class="section">Note on codes in AUTO(2) namespace</div>
 * The format is usually "{@code AUTO2:}<var>n</var>,<var>factor</var>,<var>λ₀</var>,<var>φ₀</var>"
 * where <var>n</var> is a number between 42001 and 42005 inclusive, <var>factor</var> is a conversion
 * factor from the CRS units to metres (e.g. 0.3048 for a CRS with axes in feet) and (<var>λ₀</var>,<var>φ₀</var>)
 * is the longitude and latitude of a point in the projection centre.
 *
 * <div class="note"><b>Examples:</b>
 * {@code "AUTO2:42001,1,-100,45"} identifies a Universal Transverse Mercator (UTM) projection
 * for a zone containing the point at (45°N, 100°W) with axes in metres.</div>
 *
 * Codes in the {@code "AUTO"} namespace are the same than codes in the {@code "AUTO2"} namespace, except that
 * the {@linkplain org.apache.sis.measure.Units#valueOfEPSG(int) EPSG code} of the desired unit of measurement
 * was used instead than the unit factor.
 * The {@code "AUTO"} namespace was defined in the <cite>Web Map Service</cite> (WMS) 1.1.1 specification
 * while the {@code "AUTO2"} namespace is defined in WMS 1.3.0.
 * In Apache SIS implementation, the unit parameter (either as factor or as EPSG code) is optional and defaults to metres.
 *
 * <p>In the {@code AUTO(2):42001} case, the UTM zone is calculated as specified in WMS 1.3 specification,
 * i.e. <strong>without</strong> taking in account the Norway and Svalbard special cases and without
 * switching to polar stereographic projections for high latitudes.</p>
 *
 * @author  Martin Desruisseaux (IRD, Geomatys)
 * @version 0.8
 *
 * @see CommonCRS
 *
 * @since 0.7
 * @module
 */
public class CommonAuthorityFactory extends GeodeticAuthorityFactory implements CRSAuthorityFactory {
    /**
     * The {@value} prefix for a code identified by parameters.
     * This is defined in annexes B.7 to B.11 of WMS 1.3 specification.
     * The {@code "AUTO(2)"} namespaces are not considered by SIS as real authorities.
     */
    private static final String AUTO2 = "AUTO2";

    /**
     * The namespaces of codes defined by OGC.
     *
     * @see #getCodeSpaces()
     */
    private static final Set<String> CODESPACES = Collections.unmodifiableSet(
            new LinkedHashSet<>(Arrays.asList(Constants.OGC, Constants.CRS, "AUTO", AUTO2)));

    /**
     * The bit for saying that a namespace is the legacy {@code "AUTO"} namespace.
     */
    private static final int LEGACY_MASK = 0x80000000;

    /**
     * First code in the AUTO(2) namespace.
     */
    private static final int FIRST_PROJECTION_CODE = 42001;

    /**
     * Names of objects in the AUTO(2) namespace for codes from 42001 to 42005 inclusive.
     * Those names are defined in annexes B.7 to B.11 of WMS 1.3 specification.
     *
     * @see #getDescriptionText(String)
     */
    private static final String[] PROJECTION_NAMES = {
        "WGS 84 / Auto UTM",
        "WGS 84 / Auto Tr. Mercator",
        "WGS 84 / Auto Orthographic",
        "WGS 84 / Auto Equirectangular",
        "WGS 84 / Auto Mollweide"
    };

    /**
     * The parameter separator for codes in the {@code "AUTO(2)"} namespace.
     */
    static final char SEPARATOR = ',';

    /**
     * The codes known to this factory, associated with their CRS type. This is set to an empty map
     * at {@code CommonAuthorityFactory} construction time and filled only when first needed.
     */
    private final Map<String,Class<?>> codes;

    /**
     * The "Computer display" reference system (CRS:1). Created when first needed.
     *
     * @see #displayCRS()
     */
    private CoordinateReferenceSystem displayCRS;

    /**
     * The coordinate system for map projection in metres, created when first needed.
     */
    private volatile CartesianCS projectedCS;

    /**
     * Constructs a default factory for the {@code CRS} authority.
     */
    public CommonAuthorityFactory() {
        codes = new LinkedHashMap<>();
    }

    /**
     * Returns the specification that defines the codes recognized by this factory. The definitive source for this
     * factory is OGC <a href="http://www.opengeospatial.org/standards/wms">Web Map Service</a> (WMS) specification,
     * also available as the ISO 19128 <cite>Geographic Information — Web map server interface</cite> standard.
     *
     * <p>While the authority is WMS, the {@linkplain org.apache.sis.xml.IdentifierSpace#getName() namespace}
     * of that authority is set to {@code "OGC"}. Apache SIS does that for consistency with the namespace used
     * in URNs (for example {@code "urn:ogc:def:crs:OGC:1.3:CRS84"}).</p>
     *
     * @return the <cite>"Web Map Service"</cite> authority.
     *
     * @see #getCodeSpaces()
     * @see Citations#WMS
     */
    @Override
    public Citation getAuthority() {
        return Citations.WMS;
    }

    /**
     * Rewrites the given code in a canonical format.
     * If the code can not be reformatted, then this method returns {@code null}.
     */
    static String reformat(final String code) {
        try {
            return format(Integer.parseInt(code.substring(skipNamespace(code) & ~LEGACY_MASK)));
        } catch (NoSuchAuthorityCodeException | NumberFormatException e) {
            Logging.recoverableException(Logging.getLogger(Loggers.CRS_FACTORY), CommonAuthorityFactory.class, "reformat", e);
            return null;
        }
    }

    /**
     * Returns the index where the code begins, ignoring spaces and the {@code "OGC"}, {@code "CRS"}, {@code "AUTO"},
     * {@code "AUTO1"} or {@code "AUTO2"} namespaces if present. If a namespace is found and is a legacy one, then
     * this {@link #LEGACY_MASK} bit will be set.
     *
     * @return index where the code begin, possibly with the {@link #LEGACY_MASK} bit set.
     * @throws NoSuchAuthorityCodeException if an authority is present but is not one of the recognized authorities.
     */
    private static int skipNamespace(final String code) throws NoSuchAuthorityCodeException {
        int isLegacy = 0;
        int s = code.indexOf(Constants.DEFAULT_SEPARATOR);
        if (s >= 0) {
            final int end   = CharSequences.skipTrailingWhitespaces(code, 0, s);
            final int start = CharSequences.skipLeadingWhitespaces (code, 0, end);
            if (!regionMatches(Constants.CRS, code, start, end) &&
                !regionMatches(Constants.OGC, code, start, end))
            {
                boolean isRecognized = false;
                final int length = AUTO2.length() - 1;
                if (code.regionMatches(true, start, AUTO2, 0, length)) {
                    switch (end - start - length) {         // Number of extra characters after "AUTO".
                        case 0: {                           // Namespace is exactly "AUTO" (ignoring case).
                            isRecognized = true;
                            isLegacy = LEGACY_MASK;
                            break;
                        }
                        case 1: {                           // Namespace has one more character than "AUTO".
                            final char c = code.charAt(end - 1);
                            isRecognized = (c >= '1' && c <= '2');
                            if (c == '1') {
                                isLegacy = LEGACY_MASK;
                            }
                        }
                    }
                }
                if (!isRecognized) {
                    throw new NoSuchAuthorityCodeException(Resources.format(Resources.Keys.UnknownAuthority_1,
                            CharSequences.trimWhitespaces(code, 0, s)), Constants.OGC, code);
                }
            }
        }
        s = CharSequences.skipLeadingWhitespaces(code, s+1, code.length());
        /*
         * Above code removed the "CRS" part when it is used as a namespace, as in "CRS:84".
         * The code below removes the "CRS" prefix when it is concatenated within the code,
         * as in "CRS84". Together, those two checks handle redundant codes like "CRS:CRS84"
         * (malformed code, but seen in practice).
         */
        if (code.regionMatches(true, s, Constants.CRS, 0, Constants.CRS.length())) {
            s = CharSequences.skipLeadingWhitespaces(code, s + Constants.CRS.length(), code.length());
        }
        if (s >= code.length()) {
            throw new NoSuchAuthorityCodeException(Errors.format(Errors.Keys.EmptyArgument_1, "code"), Constants.OGC, code);
        }
        return s | isLegacy;
    }

    /**
     * Provides a complete set of the known codes provided by this factory.
     * The returned set contains a namespace followed by numeric identifiers
     * like {@code "CRS:84"}, {@code "CRS:27"}, {@code "AUTO2:42001"}, <i>etc</i>.
     *
     * @param  type  the spatial reference objects type.
     * @return the set of authority codes for spatial reference objects of the given type.
     * @throws FactoryException if this method failed to provide the set of codes.
     */
    @Override
    public Set<String> getAuthorityCodes(final Class<? extends IdentifiedObject> type) throws FactoryException {
        ArgumentChecks.ensureNonNull("type", type);
        if (!type.isAssignableFrom(SingleCRS.class) && !SingleCRS.class.isAssignableFrom(type)) {
            return Collections.emptySet();
        }
        synchronized (codes) {
            if (codes.isEmpty()) {
                add(Constants.CRS1,  EngineeringCRS.class);
                add(Constants.CRS27, GeographicCRS.class);
                add(Constants.CRS83, GeographicCRS.class);
                add(Constants.CRS84, GeographicCRS.class);
                add(Constants.CRS88, VerticalCRS.class);
                for (int code = FIRST_PROJECTION_CODE; code < FIRST_PROJECTION_CODE + PROJECTION_NAMES.length; code++) {
                    add(code, ProjectedCRS.class);
                }
            }
        }
        return new FilteredCodes(codes, type).keySet();
    }

    /**
     * Formats the given code with a {@code "CRS:"} or {@code "AUTO2:"} prefix.
     */
    private static String format(final int code) {
        return ((code >= FIRST_PROJECTION_CODE) ? AUTO2 : Constants.CRS) + Constants.DEFAULT_SEPARATOR + code;
    }

    /**
     * Adds an element in the {@link #codes} map, witch check against duplicated values.
     */
    private void add(final int code, final Class<? extends SingleCRS> type) throws FactoryException {
        assert (code >= FIRST_PROJECTION_CODE) == (ProjectedCRS.class.isAssignableFrom(type)) : code;
        if (codes.put(format(code), type) != null) {
            throw new FactoryException();    // Should never happen, but we are paranoiac.
        }
    }

    /**
     * Returns the namespaces defined by the OGC specifications implemented by this factory.
     * At the difference of other factories, the namespaces of {@code CommonAuthorityFactory}
     * are quite different than the {@linkplain #getAuthority() authority} title or identifier:
     *
     * <ul>
     *   <li><b>Authority:</b> {@code "WMS"} (for <cite>"Web Map Services"</cite>)</li>
     *   <li><b>Namespaces:</b> {@code "CRS"}, {@code "AUTO"}, {@code "AUTO2"}.
     *       The {@code "OGC"} namespace is also accepted for compatibility reason,
     *       but its scope is wider than the above-cited namespaces.</li>
     * </ul>
     *
     * @return a set containing at least the {@code "CRS"}, {@code "AUTO"} and {@code "AUTO2"} strings.
     *
     * @see #getAuthority()
     * @see Citations#WMS
     */
    @Override
    public Set<String> getCodeSpaces() {
        return CODESPACES;
    }

    /**
     * Returns a description of the object corresponding to a code.
     * The description can be used for example in a combo box in a graphical user interface.
     *
     * <p>Codes in the {@code "AUTO(2)"} namespace do not need parameters for this method.
     * But if parameters are nevertheless specified, then they will be taken in account.</p>
     *
     * <table class="sis">
     *   <caption>Examples</caption>
     *   <tr><th>Argument value</th>                <th>Return value</th></tr>
     *   <tr><td>{@code CRS:84}</td>                <td>WGS 84</td></tr>
     *   <tr><td>{@code AUTO2:42001}</td>           <td>WGS 84 / Auto UTM</td></tr>
     *   <tr><td>{@code AUTO2:42001,1,-100,45}</td> <td>WGS 84 / UTM zone 47N</td></tr>
     * </table>
     *
     * @param  code  value in the CRS or AUTO(2) code space.
     * @return a description of the object.
     * @throws NoSuchAuthorityCodeException if the specified {@code code} was not found.
     * @throws FactoryException if an error occurred while fetching the description.
     */
    @Override
    public InternationalString getDescriptionText(final String code) throws FactoryException {
        final int s = skipNamespace(code) & ~LEGACY_MASK;
        final String localCode = code.substring(s, CharSequences.skipTrailingWhitespaces(code, s, code.length()));
        if (localCode.indexOf(SEPARATOR) < 0) {
            /*
             * For codes in the "AUTO(2)" namespace without parameters, we can not rely on the default implementation
             * since it would fail to create the ProjectedCRS instance. Instead we return a generic description.
             * Note that we do not execute this block if parametes were specified. If there is parameters,
             * then we instead rely on the default implementation for a more accurate description text.
             */
            final int codeValue;
            try {
                codeValue = Integer.parseInt(localCode);
            } catch (NumberFormatException exception) {
                throw noSuchAuthorityCode(localCode, code, exception);
            }
            final int i = codeValue - FIRST_PROJECTION_CODE;
            if (i >= 0 && i < PROJECTION_NAMES.length) {
                return new SimpleInternationalString(PROJECTION_NAMES[i]);
            }
        }
        return new SimpleInternationalString(createCoordinateReferenceSystem(localCode).getName().getCode());
    }

    /**
     * Creates an object from the specified code.
     * The default implementation delegates to {@link #createCoordinateReferenceSystem(String)}.
     *
     * @throws FactoryException if the object creation failed.
     */
    @Override
    public IdentifiedObject createObject(final String code) throws FactoryException {
        return createCoordinateReferenceSystem(code);
    }

    /**
     * Creates a coordinate reference system from the specified code.
     * This method performs the following steps:
     *
     * <ol>
     *   <li>Skip the {@code "OGC"}, {@code "CRS"}, {@code "AUTO"}, {@code "AUTO1"} or {@code "AUTO2"} namespace
     *       if present (ignoring case). All other namespaces will cause an exception to be thrown.</li>
     *   <li>Skip the {@code "CRS"} prefix if present. This additional check is for accepting codes like
     *       {@code "OGC:CRS84"} (not a valid CRS code, but seen in practice).</li>
     *   <li>In the remaining text, interpret the integer value as documented in this class javadoc.
     *       Note that some codes require coma-separated parameters after the integer value.</li>
     * </ol>
     *
     * @param  code  value allocated by OGC.
     * @return the coordinate reference system for the given code.
     * @throws FactoryException if the object creation failed.
     */
    @Override
    public CoordinateReferenceSystem createCoordinateReferenceSystem(final String code) throws FactoryException {
        ArgumentChecks.ensureNonNull("code", code);
        final String localCode;
        final boolean isLegacy;
        String complement = null;
        { // Block for keeping 'start' and 'end' variables locale.
            int start = skipNamespace(code);
            isLegacy = (start & LEGACY_MASK) != 0;
            start &= ~LEGACY_MASK;
            final int startOfParameters = code.indexOf(SEPARATOR, start);
            int end = CharSequences.skipTrailingWhitespaces(code, start, code.length());
            if (startOfParameters >= 0) {
                complement = code.substring(startOfParameters + 1);
                end = CharSequences.skipTrailingWhitespaces(code, start, startOfParameters);
            }
            localCode = code.substring(start, end);
        }
        int codeValue = 0;
        double[] parameters = ArraysExt.EMPTY_DOUBLE;
        try {
            codeValue = Integer.parseInt(localCode);
            if (complement != null) {
                parameters = CharSequences.parseDoubles(complement, SEPARATOR);
            }
        } catch (NumberFormatException exception) {
            throw noSuchAuthorityCode(localCode, code, exception);
        }
        /*
         * At this point we have isolated the code value from the parameters (if any). Verify the number of arguments.
         * Then codes in the AUTO(2) namespace are delegated to a separated method while codes in the CRS namespaces
         * are handled below.
         */
        final int count = parameters.length;
        if (codeValue >= FIRST_PROJECTION_CODE) {
            int expected;
            short errorKey = 0;
            if (count < (expected = 2)) {
                errorKey = Errors.Keys.TooFewArguments_2;
            } else if (count > (expected = 3)) {
                errorKey = Errors.Keys.TooManyArguments_2;
            }
            if (errorKey == 0) {
                return createAuto(code, codeValue, isLegacy,
                        (count > 2) ? parameters[0] : isLegacy ? Constants.EPSG_METRE : 1,
                                      parameters[count - 2],
                                      parameters[count - 1]);
            }
            throw new NoSuchAuthorityCodeException(Errors.format(errorKey, expected, count), AUTO2, localCode, code);
        }
        if (count != 0) {
            throw new NoSuchAuthorityCodeException(Errors.format(Errors.Keys.UnexpectedCharactersAfter_2,
                    localCode, complement), Constants.CRS, localCode, code);
        }
        final CommonCRS crs;
        switch (codeValue) {
            case Constants.CRS1:  return displayCRS();
            case Constants.CRS84: crs = CommonCRS.WGS84; break;
            case Constants.CRS83: crs = CommonCRS.NAD83; break;
            case Constants.CRS27: crs = CommonCRS.NAD27; break;
            case Constants.CRS88: return CommonCRS.Vertical.NAVD88.crs();
            default: throw noSuchAuthorityCode(localCode, code, null);
        }
        return crs.normalizedGeographic();
    }

    /**
     * Creates a projected CRS from parameters in the {@code AUTO(2)} namespace.
     *
     * @param  code        the user-specified code, used only for error reporting.
     * @param  projection  the projection code (e.g. 42001).
     * @param  isLegacy    {@code true} if the code was found in {@code "AUTO"} or {@code "AUTO1"} namespace.
     * @param  factor      the multiplication factor for the unit of measurement.
     * @param  longitude   a longitude in the desired projection zone.
     * @param  latitude    a latitude in the desired projection zone.
     * @return the projected CRS for the given projection and parameters.
     */
    @SuppressWarnings("null")
    private ProjectedCRS createAuto(final String code, final int projection, final boolean isLegacy,
            final double factor, final double longitude, final double latitude) throws FactoryException
    {
        Boolean isUTM = null;
        String method = null;
        String param  = null;
        switch (projection) {
            /*
             * 42001: Universal Transverse Mercator   —   central meridian must be in the center of a UTM zone.
             * 42002: Transverse Mercator             —   like 42001 except that central meridian can be anywhere.
             * 42003: WGS 84 / Auto Orthographic      —   defined by "Central_Meridian" and "Latitude_of_Origin".
             * 42004: WGS 84 / Auto Equirectangular   —   defined by "Central_Meridian" and "Standard_Parallel_1".
             * 42005: WGS 84 / Auto Mollweide         —   defined by "Central_Meridian" only.
             */
            case 42001: isUTM  = true; break;
            case 42002: isUTM  = (latitude == 0) && (Zoner.UTM.centralMeridian(Zoner.UTM.zone(0, longitude)) == longitude); break;
            case 42003: method = "Orthographic";       param = Constants.LATITUDE_OF_ORIGIN;  break;
            case 42004: method = "Equirectangular";    param = Constants.STANDARD_PARALLEL_1; break;
            case 42005: method = "Mollweide";                                                 break;
            default: throw noSuchAuthorityCode(String.valueOf(projection), code, null);
        }
        /*
         * For the (Universal) Transverse Mercator case (AUTO:42001 and 42002), we delegate to the CommonCRS
         * enumeration if possible because CommonCRS will itself delegate to the EPSG factory if possible.
         * The Math.signum(latitude) instruction is for preventing "AUTO:42001" to handle the UTM special cases
         * (Norway and Svalbard) or to switch on the Universal Polar Stereographic projection for high latitudes,
         * because the WMS specification does not said that we should.
         */
        final CommonCRS datum = CommonCRS.WGS84;
        final GeographicCRS baseCRS;                // To be set, directly or indirectly, to WGS84.geographic().
        final ProjectedCRS crs;                     // Temporary UTM projection, for extracting other properties.
        CartesianCS cs;                             // Coordinate system with (E,N) axes in metres.
        try {
            if (isUTM != null && isUTM) {
                crs = datum.universal(Math.signum(latitude), longitude);
                if (factor == (isLegacy ? Constants.EPSG_METRE : 1)) {
                    return crs;
                }
                baseCRS = crs.getBaseCRS();
                cs = crs.getCoordinateSystem();
            } else {
                cs = projectedCS;
                if (cs == null) {
                    crs = datum.universal(Math.signum(latitude), longitude);
                    projectedCS = cs = crs.getCoordinateSystem();
                    baseCRS = crs.getBaseCRS();
                } else {
                    crs = null;
                    baseCRS = datum.geographic();
                }
            }
            /*
             * At this point we got a coordinate system with axes in metres.
             * If the user asked for another unit of measurement, change the axes now.
             */
            Unit<Length> unit;
            if (isLegacy) {
                unit = createUnitFromEPSG(factor).asType(Length.class);
            } else {
                unit = Units.METRE;
                if (factor != 1) unit = unit.multiply(factor);
            }
            if (!Units.METRE.equals(unit)) {
                cs = (CartesianCS) CoordinateSystems.replaceLinearUnit(cs, unit);
            }
            /*
             * Set the projection name, operation method and parameters. The parameters for the Transverse Mercator
             * projection are a little bit more tedious to set, so we use a convenience method for that.
             */
            final GeodeticObjectBuilder builder = new GeodeticObjectBuilder();
            if (isUTM != null) {
                if (isUTM && crs != null) {
                    builder.addName(crs.getName());
                } // else default to the conversion name, which is "Transverse Mercator".
                builder.setTransverseMercator(isUTM ? Zoner.UTM : Zoner.ANY, latitude, longitude);
            } else {
                builder.setConversionMethod(method)
                       .addName(PROJECTION_NAMES[projection - FIRST_PROJECTION_CODE])
                       .setParameter(Constants.CENTRAL_MERIDIAN, longitude, Units.DEGREE);
                if (param != null) {
                    builder.setParameter(param, latitude, Units.DEGREE);
                }
            }
            return builder.createProjectedCRS(baseCRS, cs);
        } catch (IllegalArgumentException e) {
            throw noSuchAuthorityCode(String.valueOf(projection), code, e);
        }
    }

    /**
     * Returns the unit of measurement for the given EPSG code.
     * This is used only for codes in the legacy {@code "AUTO"} namespace.
     */
    private static Unit<?> createUnitFromEPSG(final double code) throws NoSuchAuthorityCodeException {
        String message = null;      // Error message to be used only in case of failure.
        final String s;             // The string representation of the code, to be used only in case of failure.
        final int c = (int) code;
        if (c == code) {
            final Unit<?> unit = Units.valueOfEPSG(c);
            if (Units.isLinear(unit)) {
                return unit;
            } else if (unit != null) {
                message = Errors.format(Errors.Keys.NonLinearUnit_1, unit);
            }
            s = String.valueOf(c);
        } else {
            s = String.valueOf(code);
        }
        if (message == null) {
            message = Resources.format(Resources.Keys.NoSuchAuthorityCode_3, Constants.EPSG, Unit.class, s);
        }
        throw new NoSuchAuthorityCodeException(message, Constants.EPSG, s);
    }

    /**
     * Returns the "Computer display" reference system (CRS:1). This is rarely used.
     */
    private synchronized CoordinateReferenceSystem displayCRS() throws FactoryException {
        if (displayCRS == null) {
            final CSFactory csFactory = DefaultFactories.forBuildin(CSFactory.class);
            final CartesianCS cs = csFactory.createCartesianCS(
                    Collections.singletonMap(CartesianCS.NAME_KEY, "Computer display"),
                    csFactory.createCoordinateSystemAxis(Collections.singletonMap(CartesianCS.NAME_KEY, "i"), "i", AxisDirection.EAST,  Units.PIXEL),
                    csFactory.createCoordinateSystemAxis(Collections.singletonMap(CartesianCS.NAME_KEY, "j"), "j", AxisDirection.SOUTH, Units.PIXEL));

            final Map<String,Object> properties = new HashMap<>(4);
            properties.put(EngineeringDatum.NAME_KEY, cs.getName());
            properties.put(EngineeringDatum.ANCHOR_POINT_KEY, "Origin is in upper left.");
            displayCRS = DefaultFactories.forBuildin(CRSFactory.class).createEngineeringCRS(properties,
                         DefaultFactories.forBuildin(DatumFactory.class).createEngineeringDatum(properties), cs);
        }
        return displayCRS;
    }

    /**
     * Creates an exception for an unknown authority code.
     *
     * @param  localCode  the unknown authority code, without namespace.
     * @param  code       the unknown authority code as specified by the user (may include namespace).
     * @param  cause      the failure cause, or {@code null} if none.
     * @return an exception initialized with an error message built from the specified information.
     */
    private static NoSuchAuthorityCodeException noSuchAuthorityCode(String localCode, String code, Exception cause) {
        return (NoSuchAuthorityCodeException) new NoSuchAuthorityCodeException(Resources.format(Resources.Keys.NoSuchAuthorityCode_3,
                Constants.OGC, CoordinateReferenceSystem.class, localCode),
                Constants.OGC, localCode, code).initCause(cause);
    }
}
