/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *
 *   (C) 2003-2006, Geotools Project Managment Committee (PMC)
 *   (C) 2002, 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; either
 *    version 2.1 of the License, or (at your option) any later version.
 *
 *    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.wkt;

// J2SE dependencies
import java.io.BufferedReader;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.units.NonSI;
import javax.units.SI;
import javax.units.Unit;

// OpenGIS dependencies
import org.opengis.metadata.citation.Citation;
import org.opengis.parameter.ParameterDescriptor;
import org.opengis.parameter.ParameterNotFoundException;
import org.opengis.parameter.ParameterValue;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.IdentifiedObject;
import org.opengis.referencing.NoSuchIdentifierException;
import org.opengis.referencing.crs.CRSFactory;
import org.opengis.referencing.crs.CompoundCRS;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.crs.DerivedCRS;
import org.opengis.referencing.crs.EngineeringCRS;
import org.opengis.referencing.crs.GeocentricCRS;
import org.opengis.referencing.crs.GeographicCRS;
import org.opengis.referencing.crs.ProjectedCRS;
import org.opengis.referencing.crs.VerticalCRS;
import org.opengis.referencing.cs.AxisDirection;
import org.opengis.referencing.cs.CSFactory;
import org.opengis.referencing.cs.CoordinateSystem;
import org.opengis.referencing.cs.CoordinateSystemAxis;
import org.opengis.referencing.datum.Datum;  // For javadoc
import org.opengis.referencing.datum.DatumFactory;
import org.opengis.referencing.datum.Ellipsoid;
import org.opengis.referencing.datum.EngineeringDatum;
import org.opengis.referencing.datum.GeodeticDatum;
import org.opengis.referencing.datum.PrimeMeridian;
import org.opengis.referencing.datum.VerticalDatum;
import org.opengis.referencing.datum.VerticalDatumType;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransformFactory;
import org.opengis.referencing.operation.NoninvertibleTransformException;
import org.opengis.referencing.operation.OperationMethod;

// Geotools dependencies
import org.geotools.factory.Hints;
import org.geotools.metadata.iso.citation.Citations;
import org.geotools.referencing.ReferencingFactoryFinder;
import org.geotools.referencing.NamedIdentifier;
import org.geotools.referencing.datum.BursaWolfParameters;
import org.geotools.referencing.datum.DefaultGeodeticDatum;
import org.geotools.referencing.datum.DefaultPrimeMeridian;
import org.geotools.referencing.datum.DefaultVerticalDatum;
import org.geotools.referencing.cs.AbstractCS;
import org.geotools.referencing.cs.DefaultCoordinateSystemAxis;
import org.geotools.referencing.factory.ReferencingFactoryContainer;
import org.geotools.resources.Arguments;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.i18n.ErrorKeys;


/**
 * Parser for
 * <A HREF="http://geoapi.sourceforge.net/snapshot/javadoc/org/opengis/referencing/doc-files/WKT.html"><cite>Well
 * Known Text</cite> (WKT)</A>. This parser can parse {@linkplain MathTransform math transform}
 * objects as well, which is part of the WKT's {@code FITTED_CS} element.
 *
 * @since 2.0
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/referencing/src/main/java/org/geotools/referencing/wkt/Parser.java $
 * @version $Id: Parser.java 27520 2007-10-16 19:00:52Z desruisseaux $
 * @author Remi Eve
 * @author Martin Desruisseaux
 *
 * @see <A HREF="http://geoapi.sourceforge.net/snapshot/javadoc/org/opengis/referencing/doc-files/WKT.html">Well Know Text specification</A>
 * @see <A HREF="http://gdal.velocet.ca/~warmerda/wktproblems.html">OGC WKT Coordinate System Issues</A>
 */
public class Parser extends MathTransformParser {
    /**
     * {@code true} in order to allows the non-standard Oracle syntax. Oracle put the Bursa-Wolf
     * parameters straight into the {@code DATUM} elements, without enclosing them in a
     * {@code TOWGS84} element.
     */
    private static final boolean ALLOW_ORACLE_SYNTAX = true;

    /**
     * The mapping between WKT element name and the object class to be created.
     * Will be created by {@link #getTypeMap} only when first needed. Keys must
     * be upper case.
     */
    private static Map/*<String,Class<?>>*/ TYPES;

    /**
     * The factory to use for creating {@linkplain Datum datum}.
     */
    protected final DatumFactory datumFactory;

    /**
     * The factory to use for creating {@linkplain CoordinateSystem coordinate systems}.
     */
    protected final CSFactory csFactory;

    /**
     * The factory to use for creating {@linkplain CoordinateReferenceSystem
     * coordinate reference systems}.
     */
    protected final CRSFactory crsFactory;

    /**
     * Set of helper methods working on factories. Will be constructed
     * only the first time it is needed.
     *
     * @todo This field is a workaround for a limitation in current {@link CRSFactory}
     *       interface. We should remove this field if the limitation is fixed in GeoAPI.
     */
    private transient ReferencingFactoryContainer factories;

    /**
     * The list of {@linkplain AxisDirection axis directions} from their name.
     */
    private final Map directions;

    /**
     * Constructs a parser using the default set of symbols and factories.
     */
    public Parser() {
        this(Symbols.DEFAULT);
    }

    /**
     * Constructs a parser for the specified set of symbols using default factories.
     *
     * @param symbols The symbols for parsing and formatting numbers.
     *
     * @todo Pass hints in argument.
     */
    public Parser(final Symbols symbols) {
        this(symbols,
             ReferencingFactoryFinder.getDatumFactory        (null),
             ReferencingFactoryFinder.getCSFactory           (null),
             ReferencingFactoryFinder.getCRSFactory          (null),
             ReferencingFactoryFinder.getMathTransformFactory(null));
    }

    /**
     * Constructs a parser for the specified set of symbols using the specified set of factories.
     *
     * @param symbols   The symbols for parsing and formatting numbers.
     * @param factories The factories to use.
     */
    public Parser(final Symbols symbols, final ReferencingFactoryContainer factories) {
        this(symbols,
             factories.getDatumFactory(),
             factories.getCSFactory(),
             factories.getCRSFactory(),
             factories.getMathTransformFactory());
        this.factories = factories;
    }

    /**
     * Constructs a parser for the specified set of symbols using the specified factories.
     *
     * @param symbols      The symbols for parsing and formatting numbers.
     * @param datumFactory The factory to use for creating {@linkplain Datum datum}.
     * @param csFactory    The factory to use for creating {@linkplain CoordinateSystem
     *                     coordinate systems}.
     * @param crsFactory   The factory to use for creating {@linkplain CoordinateReferenceSystem
     *                     coordinate reference systems}.
     * @param mtFactory    The factory to use for creating {@linkplain MathTransform
     *                     math transform} objects.
     */
    public Parser(final Symbols                symbols,
                  final DatumFactory      datumFactory,
                  final CSFactory            csFactory,
                  final CRSFactory          crsFactory,
                  final MathTransformFactory mtFactory)
    {
        super(symbols, mtFactory);
        this.datumFactory = datumFactory;
        this. csFactory   =    csFactory;
        this.crsFactory   =   crsFactory;
        final AxisDirection[] values = AxisDirection.values();
        directions = new HashMap((int)Math.ceil((values.length+1)/0.75f), 0.75f);
        for (int i=0; i<values.length; i++) {
            directions.put(values[i].name().trim().toUpperCase(), values[i]);
        }
    }

    /**
     * Parses a coordinate reference system element.
     *
     * @param  text The text to be parsed.
     * @return The coordinate reference system.
     * @throws ParseException if the string can't be parsed.
     */
    public CoordinateReferenceSystem parseCoordinateReferenceSystem(final String text)
            throws ParseException
    {
        final Element element = getTree(text, new ParsePosition(0));
        final CoordinateReferenceSystem crs = parseCoordinateReferenceSystem(element);
        element.close();
        return crs;
    }

    /**
     * Parses a coordinate reference system element.
     *
     * @param  parent The parent element.
     * @return The next element as a {@link CoordinateReferenceSystem} object.
     * @throws ParseException if the next element can't be parsed.
     */
    private CoordinateReferenceSystem parseCoordinateReferenceSystem(final Element element)
            throws ParseException
    {
        final Object key = element.peek();
        if (key instanceof Element) {
            final String keyword = ((Element) key).keyword.trim().toUpperCase(symbols.locale);
            CoordinateReferenceSystem r = null;
            try {
                if (   "GEOGCS".equals(keyword)) return r=parseGeoGCS  (element);
                if (   "PROJCS".equals(keyword)) return r=parseProjCS  (element);
                if (   "GEOCCS".equals(keyword)) return r=parseGeoCCS  (element);
                if (  "VERT_CS".equals(keyword)) return r=parseVertCS  (element);
                if ( "LOCAL_CS".equals(keyword)) return r=parseLocalCS (element);
                if ( "COMPD_CS".equals(keyword)) return r=parseCompdCS (element);
                if ("FITTED_CS".equals(keyword)) return r=parseFittedCS(element);
            } finally {
                // Work around for simulating post-conditions in Java.
                assert isValid(r, keyword) : element;
            }
        }
        throw element.parseFailed(null, Errors.format(ErrorKeys.UNKNOW_TYPE_$1, key));
    }

    /**
     * Parses the next element in the specified <cite>Well Know Text</cite> (WKT) tree.
     *
     * @param  element The element to be parsed.
     * @return The object.
     * @throws ParseException if the element can't be parsed.
     *
     * @todo All sequences of <code>if ("FOO".equals(keyword))</code> in this method
     *       and other methods of this class and subclasses, could be optimized with
     *       a {@code switch} statement.
     */
    protected Object parse(final Element element) throws ParseException {
        final Object key = element.peek();
        if (key instanceof Element) {
            final String keyword = ((Element) key).keyword.trim().toUpperCase(symbols.locale);
            Object r = null;
            try {
                if (       "AXIS".equals(keyword)) return r=parseAxis      (element, SI.METER, true);
                if (     "PRIMEM".equals(keyword)) return r=parsePrimem    (element, NonSI.DEGREE_ANGLE);
                if (    "TOWGS84".equals(keyword)) return r=parseToWGS84   (element);
                if (   "SPHEROID".equals(keyword)) return r=parseSpheroid  (element);
                if ( "VERT_DATUM".equals(keyword)) return r=parseVertDatum (element);
                if ("LOCAL_DATUM".equals(keyword)) return r=parseLocalDatum(element);
                if (      "DATUM".equals(keyword)) return r=parseDatum     (element, DefaultPrimeMeridian.GREENWICH);
                r = parseMathTransform(element, false);
                if (r != null) {
                    return r;
                }
            } finally {
                // Work around for simulating post-conditions in Java.
                assert isValid(r, keyword) : element;
            }
        }
        return parseCoordinateReferenceSystem(element);
    }

    /**
     * Checks if the parsed object is of the expected type. This is also a way to check
     * the consistency of the {@link #TYPES} map.
     */
    private static boolean isValid(final Object parsed, final String keyword) {
        if (parsed == null) {
            // Required in order to avoid AssertionError in place of ParseException.
            return true;
        }
        final Class type = getClassOf(keyword);
        return type!=null && type.isInstance(parsed);
    }

    /**
     * Returns the properties to be given to the parsed object. This method is invoked
     * automatically by the parser for the {@linkplain Element#isRoot root element} only.
     * This method expect on input the properties parsed from the {@code AUTHORITY} element,
     * and returns on output the properties to give to the object to be created. The default
     * implementation returns the {@code properties} map unchanged. Subclasses may override
     * this method in order to add or change properties.
     * <p>
     * <strong>Example:</strong> if a subclass want to add automatically an authority code when
     * no {@code AUTHORITY} element was explicitly set in the WKT, then it may test for the
     * {@link IdentifiedObject#IDENTIFIERS_KEY} key and add automatically an entry if this
     * key was missing.
     *
     * @param  properties The properties parsed from the WKT file. Entries can be added, removed
     *         or modified directly in this map.
     * @return The properties to be given to the parsed object. This is usually {@code properties}
     *         (maybe after modifications), but could also be a new map.
     *
     * @since 2.3
     */
    protected Map alterProperties(final Map properties) {
        return properties;
    }

    /**
     * Parses an <strong>optional</strong> "AUTHORITY" element.
     * This element has the following pattern:
     *
     * <blockquote><code>
     * AUTHORITY["<name>", "<code>"]
     * </code></blockquote>
     *
     * @param  parent The parent element.
     * @param  name The name of the parent object being parsed.
     * @return A properties map with the parent name and the optional autority code.
     * @throws ParseException if the "AUTHORITY" can't be parsed.
     */
    private Map parseAuthority(final Element parent, final String name) throws ParseException {
        final boolean  isRoot = parent.isRoot();
        final Element element = parent.pullOptionalElement("AUTHORITY");
        Map properties;
        if (element == null) {
            if (isRoot) {
                properties = new HashMap(4);
                properties.put(IdentifiedObject.NAME_KEY, name);
            } else {
                properties = Collections.singletonMap(IdentifiedObject.NAME_KEY, name);
            }
        } else {
            final String auth = element.pullString("name");
            final String code = element.pullString("code");
            element.close();
            final Citation authority = Citations.fromName(auth);
            properties = new HashMap(4);
            properties.put(IdentifiedObject.       NAME_KEY, new NamedIdentifier(authority, name));
            properties.put(IdentifiedObject.IDENTIFIERS_KEY, new NamedIdentifier(authority, code));
        }
        if (isRoot) {
            properties = alterProperties(properties);
        }
        return properties;
    }

    /**
     * Parses an "UNIT" element.
     * This element has the following pattern:
     *
     * <blockquote><code>
     * UNIT["<name>", <conversion factor> {,<authority>}]
     * </code></blockquote>
     *
     * @param  parent The parent element.
     * @param  unit The contextual unit. Usually {@link SI#METRE} or {@link SI#RADIAN}.
     * @return The "UNIT" element as an {@link Unit} object.
     * @throws ParseException if the "UNIT" can't be parsed.
     *
     * @todo Authority code is currently ignored. We may consider to create a subclass of
     *       {@link Unit} which implements {@link IdentifiedObject} in a future version.
     */
    private Unit parseUnit(final Element parent, final Unit unit) throws ParseException {
        final Element element = parent.pullElement("UNIT");
        final String     name = element.pullString("name");
        final double   factor = element.pullDouble("factor");
        final Map  properties = parseAuthority(element, name);
        element.close();
        return (factor!=1) ? unit.multiply(factor) : unit;
    }

    /**
     * Parses an "AXIS" element.
     * This element has the following pattern:
     *
     * <blockquote><code>
     * AXIS["<name>", NORTH | SOUTH | EAST | WEST | UP | DOWN | OTHER]
     * </code></blockquote>
     *
     * Note: there is no AUTHORITY element for AXIS element in OGC specification. However, we
     *       accept it anyway in order to make the parser more tolerant to non-100% compliant
     *       WKT. Note that AXIS is really the only element without such AUTHORITY clause and
     *       the EPSG database provides authority code for all axis.
     *
     * @param  parent The parent element.
     * @param  unit The contextual unit. Usually {@link NonSI#DEGREE_ANGLE} or {@link SI#METRE}.
     * @param  required {@code true} if the axis is mandatory,
     *         or {@code false} if it is optional.
     * @return The "AXIS" element as a {@link CoordinateSystemAxis} object, or {@code null}
     *         if the axis was not required and there is no axis object.
     * @throws ParseException if the "AXIS" element can't be parsed.
     */
    private CoordinateSystemAxis parseAxis(final Element parent,
                                           final Unit    unit,
                                           final boolean required)
            throws ParseException
    {
        final Element element;
        if (required) {
            element = parent.pullElement("AXIS");
        } else {
            element = parent.pullOptionalElement("AXIS");
            if (element == null) {
                return null;
            }
        }
        final String         name = element.pullString     ("name");
        final Element orientation = element.pullVoidElement("orientation");
        final Map      properties = parseAuthority(element, name); // See javadoc
        element.close();
        final AxisDirection direction = (AxisDirection) directions.get(
                                        orientation.keyword.trim().toUpperCase());
        if (direction == null) {
            throw element.parseFailed(null, Errors.format(ErrorKeys.UNKNOW_TYPE_$1, orientation));
        }
        try {
            return createAxis(properties, name, direction, unit);
        } catch (FactoryException exception) {
            throw element.parseFailed(exception, null);
        }
    }

    /**
     * Creates an axis. If the name matches one of pre-defined axis, the pre-defined one
     * will be returned. This replacement help to get more success when comparing a CS
     * built from WKT against a CS built from one of Geotools's constants.
     *
     * @param  properties Name and other properties to give to the new object.
     *         If {@code null}, the abbreviation will be used as the axis name.
     * @param  abbreviation The coordinate axis abbreviation.
     * @param  direction The axis direction.
     * @param  unit The coordinate axis unit.
     * @throws FactoryException if the axis can't be created.
     */
    private CoordinateSystemAxis createAxis(Map           properties,
                                      final String        abbreviation,
                                      final AxisDirection direction,
                                      final Unit          unit)
            throws FactoryException
    {
        final CoordinateSystemAxis candidate =
                DefaultCoordinateSystemAxis.getPredefined(abbreviation, direction);
        if (candidate != null && unit.equals(candidate.getUnit())) {
            return candidate;
        }
        if (properties == null) {
            properties = Collections.singletonMap(IdentifiedObject.NAME_KEY, abbreviation);
        }
        return csFactory.createCoordinateSystemAxis(properties, abbreviation, direction, unit);
    }

    /**
     * Parses a "PRIMEM" element. This element has the following pattern:
     *
     * <blockquote><code>
     * PRIMEM["<name>", <longitude> {,<authority>}]
     * </code></blockquote>
     *
     * @param  parent The parent element.
     * @param  angularUnit The contextual unit.
     * @return The "PRIMEM" element as a {@link PrimeMeridian} object.
     * @throws ParseException if the "PRIMEM" element can't be parsed.
     */
    private PrimeMeridian parsePrimem(final Element parent, final Unit angularUnit)
            throws ParseException
    {
        final Element   element = parent.pullElement("PRIMEM");
        final String       name = element.pullString("name");
        final double  longitude = element.pullDouble("longitude");
        final Map    properties = parseAuthority(element, name);
        element.close();
        try {
            return datumFactory.createPrimeMeridian(properties, longitude, angularUnit);
        } catch (FactoryException exception) {
            throw element.parseFailed(exception, null);
        }
    }

    /**
     * Parses an <strong>optional</strong> "TOWGS84" element.
     * This element has the following pattern:
     *
     * <blockquote><code>
     * TOWGS84[<dx>, <dy>, <dz>, <ex>, <ey>, <ez>, <ppm>]
     * </code></blockquote>
     *
     * @param  parent The parent element.
     * @return The "TOWGS84" element as a {@link BursaWolfParameters} object,
     *         or {@code null} if no "TOWGS84" has been found.
     * @throws ParseException if the "TOWGS84" can't be parsed.
     */
    private static BursaWolfParameters parseToWGS84(final Element parent)
            throws ParseException
    {
        final Element element = parent.pullOptionalElement("TOWGS84");
        if (element == null) {
            return null;
        }
        final BursaWolfParameters info = new BursaWolfParameters(DefaultGeodeticDatum.WGS84);
        info.dx  = element.pullDouble("dx");
        info.dy  = element.pullDouble("dy");
        info.dz  = element.pullDouble("dz");
        if (element.peek() != null) {
            info.ex  = element.pullDouble("ex");
            info.ey  = element.pullDouble("ey");
            info.ez  = element.pullDouble("ez");
            info.ppm = element.pullDouble("ppm");
        }
        element.close();
        return info;
    }

    /**
     * Parses a "SPHEROID" element. This element has the following pattern:
     *
     * <blockquote><code>
     * SPHEROID["<name>", <semi-major axis>, <inverse flattening> {,<authority>}]
     * </code></blockquote>
     *
     * @param  parent The parent element.
     * @return The "SPHEROID" element as an {@link Ellipsoid} object.
     * @throws ParseException if the "SPHEROID" element can't be parsed.
     */
    private Ellipsoid parseSpheroid(final Element parent) throws ParseException {
        Element          element = parent.pullElement("SPHEROID");
        String              name = element.pullString("name");
        double     semiMajorAxis = element.pullDouble("semiMajorAxis");
        double inverseFlattening = element.pullDouble("inverseFlattening");
        Map           properties = parseAuthority(element, name);
        element.close();
        if (inverseFlattening == 0) {
            // Inverse flattening null is an OGC convention for a sphere.
            inverseFlattening = Double.POSITIVE_INFINITY;
        }
        try {
            return datumFactory.createFlattenedSphere(properties,
                    semiMajorAxis, inverseFlattening, SI.METER);
        } catch (FactoryException exception) {
            throw element.parseFailed(exception, null);
        }
    }

    /**
     * Parses a "PROJECTION" element. This element has the following pattern:
     *
     * <blockquote><code>
     * PROJECTION["<name>" {,<authority>}]
     * </code></blockquote>
     *
     * @param  parent The parent element.
     * @param  ellipsoid The ellipsoid, or {@code null} if none.
     * @param  linearUnit The linear unit of the parent PROJCS element, or {@code null}.
     * @param  angularUnit The angular unit of the parent GEOCS element, or {@code null}.
     * @return The "PROJECTION" element as a {@link ParameterValueGroup} object.
     * @throws ParseException if the "PROJECTION" element can't be parsed.
     */
    private ParameterValueGroup parseProjection(final Element   parent,
                                                final Ellipsoid ellipsoid,
                                                final Unit      linearUnit,
                                                final Unit      angularUnit)
        throws ParseException
    {
        final Element       element = parent.pullElement("PROJECTION");
        final String classification = element.pullString("name");
        final Map        properties = parseAuthority(element, classification);
        element.close();
        /*
         * Set the list of parameters.  NOTE: Parameters are defined in
         * the parent Element (usually a "PROJCS" element), not in this
         * "PROJECTION" element.
         *
         * We will set the semi-major and semi-minor parameters from the
         * ellipsoid first. If those values were explicitly specified in
         * a "PARAMETER" statement, they will overwrite the values inferred
         * from the ellipsoid.
         */
        final ParameterValueGroup parameters;
        try {
            parameters = mtFactory.getDefaultParameters(classification);
        } catch (NoSuchIdentifierException exception) {
            throw element.parseFailed(exception, null);
        }
        Element param = parent;
        try {
            if (ellipsoid != null) {
                final Unit axisUnit = ellipsoid.getAxisUnit();
                parameters.parameter("semi_major").setValue(ellipsoid.getSemiMajorAxis(), axisUnit);
                parameters.parameter("semi_minor").setValue(ellipsoid.getSemiMinorAxis(), axisUnit);
            }
            while ((param=parent.pullOptionalElement("PARAMETER")) != null) {
                final String         paramName  = param.pullString("name");
                final double         paramValue = param.pullDouble("value");
                final ParameterValue parameter  = parameters.parameter(paramName);
                final Unit expected = ((ParameterDescriptor)parameter.getDescriptor()).getUnit();
                if (expected!=null && !Unit.ONE.equals(expected)) {
                    if (linearUnit!=null && SI.METER.isCompatible(expected)) {
                        parameter.setValue(paramValue, linearUnit);
                        continue;
                    }
                    if (angularUnit!=null && SI.RADIAN.isCompatible(expected)) {
                        parameter.setValue(paramValue, angularUnit);
                        continue;
                    }
                }
                parameter.setValue(paramValue);
            }
        } catch (ParameterNotFoundException exception) {
            throw param.parseFailed(exception, Errors.format(ErrorKeys.UNEXPECTED_PARAMETER_$1,
                                                             exception.getParameterName()));
        }
        return parameters;
    }

    /**
     * Parses a "DATUM" element. This element has the following pattern:
     *
     * <blockquote><code>
     * DATUM["<name>", <spheroid> {,<to wgs84>} {,<authority>}]
     * </code></blockquote>
     *
     * @param  parent The parent element.
     * @param  meridian the prime meridian.
     * @return The "DATUM" element as a {@link GeodeticDatum} object.
     * @throws ParseException if the "DATUM" element can't be parsed.
     */
    private GeodeticDatum parseDatum(final Element parent,
                                     final PrimeMeridian meridian)
            throws ParseException
    {
        Element             element = parent.pullElement("DATUM");
        String                 name = element.pullString("name");
        Ellipsoid         ellipsoid = parseSpheroid(element);
        BursaWolfParameters toWGS84 = parseToWGS84(element); // Optional; may be null.
        Map              properties = parseAuthority(element, name);
        if (ALLOW_ORACLE_SYNTAX && (toWGS84 == null) && (element.peek() instanceof Number)) {
            toWGS84     = new BursaWolfParameters(DefaultGeodeticDatum.WGS84);
            toWGS84.dx  = element.pullDouble("dx");
            toWGS84.dy  = element.pullDouble("dy");
            toWGS84.dz  = element.pullDouble("dz");
            toWGS84.ex  = element.pullDouble("ex");
            toWGS84.ey  = element.pullDouble("ey");
            toWGS84.ez  = element.pullDouble("ez");
            toWGS84.ppm = element.pullDouble("ppm");
        }
        element.close();
        if (toWGS84 != null) {
            if (!(properties instanceof HashMap)) {
                properties = new HashMap(properties);
            }
            properties.put(DefaultGeodeticDatum.BURSA_WOLF_KEY, toWGS84);
        }
        try {
            return datumFactory.createGeodeticDatum(properties, ellipsoid, meridian);
        } catch (FactoryException exception) {
            throw element.parseFailed(exception, null);
        }
    }

    /**
     * Parses a "VERT_DATUM" element. This element has the following pattern:
     *
     * <blockquote><code>
     * VERT_DATUM["<name>", <datum type> {,<authority>}]
     * </code></blockquote>
     *
     * @param  parent The parent element.
     * @return The "VERT_DATUM" element as a {@link VerticalDatum} object.
     * @throws ParseException if the "VERT_DATUM" element can't be parsed.
     */
    private VerticalDatum parseVertDatum(final Element parent) throws ParseException {
        final Element element = parent.pullElement("VERT_DATUM");
        final String     name = element.pullString ("name");
        final int       datum = element.pullInteger("datum");
        final Map  properties = parseAuthority(element, name);
        element.close();
        final VerticalDatumType type = DefaultVerticalDatum.getVerticalDatumTypeFromLegacyCode(datum);
        if (type == null) {
            throw element.parseFailed(null, Errors.format(ErrorKeys.UNKNOW_TYPE_$1, new Integer(datum)));
        }
        try {
            return datumFactory.createVerticalDatum(properties, type);
        } catch (FactoryException exception) {
            throw element.parseFailed(exception, null);
        }
    }

    /**
     * Parses a "LOCAL_DATUM" element. This element has the following pattern:
     *
     * <blockquote><code>
     * LOCAL_DATUM["<name>", <datum type> {,<authority>}]
     * </code></blockquote>
     *
     * @param  parent The parent element.
     * @return The "LOCAL_DATUM" element as an {@link EngineeringDatum} object.
     * @throws ParseException if the "LOCAL_DATUM" element can't be parsed.
     *
     * @todo The vertical datum type is currently ignored.
     */
    private EngineeringDatum parseLocalDatum(final Element parent) throws ParseException {
        final Element element = parent.pullElement("LOCAL_DATUM");
        final String     name = element.pullString ("name");
        final int       datum = element.pullInteger("datum");
        final Map  properties = parseAuthority(element, name);
        element.close();
        try {
            return datumFactory.createEngineeringDatum(properties);
        } catch (FactoryException exception) {
            throw element.parseFailed(exception, null);
        }
    }

    /**
     * Parses a "LOCAL_CS" element.
     * This element has the following pattern:
     *
     * <blockquote><code>
     * LOCAL_CS["<name>", <local datum>, <unit>, <axis>, {,<axis>}* {,<authority>}]
     * </code></blockquote>
     *
     * @param  parent The parent element.
     * @return The "LOCAL_CS" element as an {@link EngineeringCRS} object.
     * @throws ParseException if the "LOCAL_CS" element can't be parsed.
     *
     * @todo The coordinate system used is always a Geotools implementation, since we don't
     *       know which method to invokes in the {@link CSFactory} (is it a cartesian
     *       coordinate system? a spherical one? etc.).
     */
    private EngineeringCRS parseLocalCS(final Element parent) throws ParseException {
        Element           element = parent.pullElement("LOCAL_CS");
        String               name = element.pullString("name");
        EngineeringDatum    datum = parseLocalDatum(element);
        Unit           linearUnit = parseUnit(element, SI.METER);
        CoordinateSystemAxis axis = parseAxis(element, linearUnit, true);
        List                 list = new ArrayList();
        do {
            list.add(axis);
            axis = parseAxis(element, linearUnit, false);
        } while (axis != null);
        final Map properties = parseAuthority(element, name);
        element.close();
        final CoordinateSystem cs;
        cs = new AbstractCS(Collections.singletonMap("name", name),
                 (CoordinateSystemAxis[]) list.toArray(new CoordinateSystemAxis[list.size()]));
        try {
            return crsFactory.createEngineeringCRS(properties, datum, cs);
        } catch (FactoryException exception) {
            throw element.parseFailed(exception, null);
        }
    }

    /**
     * Parses a "GEOCCS" element.
     * This element has the following pattern:
     *
     * <blockquote><code>
     * GEOCCS["<name>", <datum>, <prime meridian>,  <linear unit>
     *        {,<axis> ,<axis> ,<axis>} {,<authority>}]
     * </code></blockquote>
     *
     * @param  parent The parent element.
     * @return The "GEOCCS" element as a {@link GeocentricCRS} object.
     * @throws ParseException if the "GEOCCS" element can't be parsed.
     */
    private GeocentricCRS parseGeoCCS(final Element parent) throws ParseException {
        final Element        element = parent.pullElement("GEOCCS");
        final String            name = element.pullString("name");
        final Map         properties = parseAuthority(element, name);
        final PrimeMeridian meridian = parsePrimem   (element, NonSI.DEGREE_ANGLE);
        final GeodeticDatum    datum = parseDatum    (element, meridian);
        final Unit        linearUnit = parseUnit     (element, SI.METER);
        CoordinateSystemAxis axis0, axis1, axis2;
        axis0 = parseAxis(element, linearUnit, false);
        try {
            if (axis0 != null) {
                axis1 = parseAxis(element, linearUnit, true);
                axis2 = parseAxis(element, linearUnit, true);
            } else {
                // Those default values are part of WKT specification.
                axis0 = createAxis(null, "X", AxisDirection.OTHER, linearUnit);
                axis1 = createAxis(null, "Y", AxisDirection.EAST,  linearUnit);
                axis2 = createAxis(null, "Z", AxisDirection.NORTH, linearUnit);
            }
            element.close();
            return crsFactory.createGeocentricCRS(properties, datum,
                    csFactory.createCartesianCS(properties, axis0, axis1, axis2));
        } catch (FactoryException exception) {
            throw element.parseFailed(exception, null);
        }
    }

    /**
     * Parses an <strong>optional</strong> "VERT_CS" element.
     * This element has the following pattern:
     *
     * <blockquote><code>
     * VERT_CS["<name>", <vert datum>, <linear unit>, {<axis>,} {,<authority>}]
     * </code></blockquote>
     *
     * @param  parent The parent element.
     * @return The "VERT_CS" element as a {@link VerticalCRS} object.
     * @throws ParseException if the "VERT_CS" element can't be parsed.
     */
    private VerticalCRS parseVertCS(final Element parent) throws ParseException {
        final Element element = parent.pullElement("VERT_CS");
        if (element == null) {
            return null;
        }
        String               name = element.pullString("name");
        VerticalDatum       datum = parseVertDatum(element);
        Unit           linearUnit = parseUnit(element, SI.METER);
        CoordinateSystemAxis axis = parseAxis(element, linearUnit, false);
        Map            properties = parseAuthority(element, name);
        element.close();
        try {
            if (axis == null) {
                axis = createAxis(null, "Z", AxisDirection.UP, linearUnit);
            }
            return crsFactory.createVerticalCRS(properties, datum,
                    csFactory.createVerticalCS(Collections.singletonMap("name", name), axis));
        } catch (FactoryException exception) {
            throw element.parseFailed(exception, null);
        }
    }

    /**
     * Parses a "GEOGCS" element. This element has the following pattern:
     *
     * <blockquote><code>
     * GEOGCS["<name>", <datum>, <prime meridian>, <angular unit>  {,<twin axes>} {,<authority>}]
     * </code></blockquote>
     *
     * @param  parent The parent element.
     * @return The "GEOGCS" element as a {@link GeographicCRS} object.
     * @throws ParseException if the "GEOGCS" element can't be parsed.
     */
    private GeographicCRS parseGeoGCS(final Element parent) throws ParseException {
        Element            element = parent.pullElement("GEOGCS");
        String                name = element.pullString("name");
        Map             properties = parseAuthority(element, name);
        Unit           angularUnit = parseUnit     (element, SI.RADIAN);
        PrimeMeridian     meridian = parsePrimem   (element, angularUnit);
        GeodeticDatum        datum = parseDatum    (element, meridian);
        CoordinateSystemAxis axis0 = parseAxis     (element, angularUnit, false);
        CoordinateSystemAxis axis1;
        try {
            if (axis0 != null) {
                axis1 = parseAxis(element, angularUnit, true);
            } else {
                // Those default values are part of WKT specification.
                axis0 = createAxis(null, "Lon", AxisDirection.EAST,  angularUnit);
                axis1 = createAxis(null, "Lat", AxisDirection.NORTH, angularUnit);
            }
            element.close();
            return crsFactory.createGeographicCRS(properties, datum,
                    csFactory.createEllipsoidalCS(properties, axis0, axis1));
        } catch (FactoryException exception) {
            throw element.parseFailed(exception, null);
        }
    }

    /**
     * Parses a "PROJCS" element.
     * This element has the following pattern:
     *
     * <blockquote><code>
     * PROJCS["<name>", <geographic cs>, <projection>, {<parameter>,}*,
     *        <linear unit> {,<twin axes>}{,<authority>}]
     * </code></blockquote>
     *
     * @param  parent The parent element.
     * @return The "PROJCS" element as a {@link ProjectedCRS} object.
     * @throws ParseException if the "GEOGCS" element can't be parsed.
     */
    private ProjectedCRS parseProjCS(final Element parent) throws ParseException {
        Element                element = parent.pullElement("PROJCS");
        String                    name = element.pullString("name");
        Map                 properties = parseAuthority(element, name);
        GeographicCRS           geoCRS = parseGeoGCS(element);
        Ellipsoid            ellipsoid = ((GeodeticDatum) geoCRS.getDatum()).getEllipsoid();
        Unit                linearUnit = parseUnit(element, SI.METER);
        Unit               angularUnit = geoCRS.getCoordinateSystem().getAxis(0).getUnit();
        ParameterValueGroup projection = parseProjection(element, ellipsoid, linearUnit, angularUnit);
        CoordinateSystemAxis     axis0 = parseAxis(element, linearUnit, false);
        CoordinateSystemAxis     axis1;
        try {
            if (axis0 != null) {
                axis1 = parseAxis(element, linearUnit, true);
            } else {
                // Those default values are part of WKT specification.
                axis0 = createAxis(null, "X", AxisDirection.EAST,  linearUnit);
                axis1 = createAxis(null, "Y", AxisDirection.NORTH, linearUnit);
            }
            element.close();
            if (factories == null) {
                final Hints hints = new Hints( Collections.EMPTY_MAP );
                hints.put(Hints.DATUM_FACTORY,          datumFactory);
                hints.put(Hints.CS_FACTORY,             csFactory);
                hints.put(Hints.CRS_FACTORY,            crsFactory);
                hints.put(Hints.MATH_TRANSFORM_FACTORY, mtFactory);
                factories = ReferencingFactoryContainer.instance(hints);
            }
            return factories.createProjectedCRS(properties, geoCRS, null, projection,
                    csFactory.createCartesianCS(properties, axis0, axis1));
        } catch (FactoryException exception) {
            throw element.parseFailed(exception, null);
        }
    }

    /**
     * Parses a "COMPD_CS" element.
     * This element has the following pattern:
     *
     * <blockquote><code>
     * COMPD_CS["<name>", <head cs>, <tail cs> {,<authority>}]
     * </code></blockquote>
     *
     * @param  parent The parent element.
     * @return The "COMPD_CS" element as a {@link CompoundCRS} object.
     * @throws ParseException if the "COMPD_CS" element can't be parsed.
     */
    private CompoundCRS parseCompdCS(final Element parent) throws ParseException {
        final CoordinateReferenceSystem[] CRS = new CoordinateReferenceSystem[2];
        Element element = parent.pullElement("COMPD_CS");
        String     name = element.pullString("name");
        Map  properties = parseAuthority(element, name);
        CRS[0] = parseCoordinateReferenceSystem(element);
        CRS[1] = parseCoordinateReferenceSystem(element);
        element.close();
        try {
            return crsFactory.createCompoundCRS(properties, CRS);
        } catch (FactoryException exception) {
            throw element.parseFailed(exception, null);
        }
    }

    /**
     * Parses a "FITTED_CS" element.
     * This element has the following pattern:
     *
     * <blockquote><code>
     * FITTED_CS["<name>", <to base>, <base cs>]
     * </code></blockquote>
     *
     * @param  parent The parent element.
     * @return The "FITTED_CS" element as a {@link CompoundCRS} object.
     * @throws ParseException if the "COMPD_CS" element can't be parsed.
     */
    private DerivedCRS parseFittedCS(final Element parent) throws ParseException {
        Element element = parent.pullElement("FITTED_CS");
        String     name = element.pullString("name");
        Map  properties = parseAuthority(element, name);
        final MathTransform toBase = parseMathTransform(element, true);
        final CoordinateReferenceSystem base = parseCoordinateReferenceSystem(element);
        final OperationMethod method = getOperationMethod();
        element.close();
        /*
         * WKT provides no informations about the underlying CS of a derived CRS.
         * We have to guess some reasonable one with arbitrary units.  We try to
         * construct the one which contains as few information as possible, in
         * order to avoid providing wrong informations.
         */
        final CoordinateSystemAxis[] axis = new CoordinateSystemAxis[toBase.getSourceDimensions()];
        final StringBuffer buffer = new StringBuffer(name);
        buffer.append(" axis ");
        final int start = buffer.length();
        try {
            for (int i=0; i<axis.length; i++) {
                final String number = String.valueOf(i);
                buffer.setLength(start);
                buffer.append(number);
                axis[i] = csFactory.createCoordinateSystemAxis(
                    Collections.singletonMap(IdentifiedObject.NAME_KEY, buffer.toString()),
                    number, AxisDirection.OTHER, Unit.ONE);
            }
            return crsFactory.createDerivedCRS(properties, method, base, toBase.inverse(),
                                               new AbstractCS(properties, axis));
        } catch (FactoryException exception) {
            throw element.parseFailed(exception, null);
        } catch (NoninvertibleTransformException exception) {
            throw element.parseFailed(exception, null);
        }
    }

    /**
     * Returns the class of the specified WKT element. For example this method returns
     * <code>{@linkplain ProjectedCRS}.class</code> for element "{@code PROJCS}".
     *
     * @param  element The WKT element name.
     * @return The GeoAPI class of the specified element, or {@code null} if unknow.
     */
    public static Class getClassOf(String element) {
        if (element == null) {
            return null;
        }
        element = element.trim().toUpperCase(Locale.US);
        final Class type = (Class) getTypeMap().get(element);
        assert type == null || type.equals(MathTransform.class)
                || element.equals(getNameOf(type)) : type;
        return type;
    }

    /**
     * Returns the WKT name of the specified object type. For example this method returns
     * "{@code PROJCS}" for type <code>{@linkplain ProjectedCRS}.class</code>.
     *
     * @param type The GeoAPI class of the specified element.
     * @return The WKT element name, or {@code null} if unknow.
     *
     * @since 2.4
     */
    public static String getNameOf(final Class/*<?>*/ type) {
        if (type != null) {
            for (final java.util.Iterator it=getTypeMap().entrySet().iterator(); it.hasNext();) {
                final Map.Entry/*<String,Class<?>>*/ entry = (Map.Entry) it.next();
                final Class candidate = (Class) entry.getValue();
                if (candidate.isAssignableFrom(type)) {
                    return (String) entry.getKey();
                }
            }
        }
        return null;
    }

    /**
     * Returns the type map.
     */
    private static Map getTypeMap() {
        if (TYPES == null) {
            final Map map = new LinkedHashMap/*<String,Class<?>>*/(25);
            map.put(        "GEOGCS",        GeographicCRS.class);
            map.put(        "PROJCS",         ProjectedCRS.class);
            map.put(        "GEOCCS",        GeocentricCRS.class);
            map.put(       "VERT_CS",          VerticalCRS.class);
            map.put(      "LOCAL_CS",       EngineeringCRS.class);
            map.put(      "COMPD_CS",          CompoundCRS.class);
            map.put(     "FITTED_CS",           DerivedCRS.class);
            map.put(          "AXIS", CoordinateSystemAxis.class);
            map.put(        "PRIMEM",        PrimeMeridian.class);
            map.put(       "TOWGS84",  BursaWolfParameters.class);
            map.put(      "SPHEROID",            Ellipsoid.class);
            map.put(    "VERT_DATUM",        VerticalDatum.class);
            map.put(   "LOCAL_DATUM",     EngineeringDatum.class);
            map.put(         "DATUM",        GeodeticDatum.class);
            map.put(      "PARAM_MT",        MathTransform.class);
            map.put(     "CONCAT_MT",        MathTransform.class);
            map.put(    "INVERSE_MT",        MathTransform.class);
            map.put("PASSTHROUGH_MT",        MathTransform.class);
            TYPES = map; // Sets the field only once completed, in order to avoid synchronisation.
                         // It is not a big deal in current implementation if two Maps are created.
        }
        return TYPES;
    }

    /**
     * Read WKT strings from the {@linkplain System#in standard input stream} and
     * reformat them to the {@linkplain System#out standard output stream}. The
     * input is read until it reach the end-of-file ({@code [Ctrl-Z]} if
     * reading from the keyboard), or until an unparsable WKT has been hit.
     * Optional arguments are:
     *
     * <TABLE CELLPADDING='0' CELLSPACING='0'>
     *   <TR><TD NOWRAP><CODE>-authority</CODE> <VAR>&lt;name&gt;</VAR></TD>
     *       <TD NOWRAP>&nbsp;The authority to prefer when choosing WKT entities names.</TD></TR>
     *   <TR><TD NOWRAP><CODE>-indentation</CODE> <VAR>&lt;value&gt;</VAR></TD>
     *       <TD NOWRAP>&nbsp;Set the indentation (0 for output on a single line)</TD></TR>
     *   <TR><TD NOWRAP><CODE>-encoding</CODE> <VAR>&lt;code&gt;</VAR></TD>
     *       <TD NOWRAP>&nbsp;Set the character encoding</TD></TR>
     *   <TR><TD NOWRAP><CODE>-locale</CODE> <VAR>&lt;language&gt;</VAR></TD>
     *       <TD NOWRAP>&nbsp;Set the language for the output (e.g. "fr" for French)</TD></TR>
     * </TABLE>
     */
    public static void main(String[] args) {
        final Arguments arguments = new Arguments(args);
        final Integer indentation = arguments.getOptionalInteger(Formattable.INDENTATION);
        final String    authority = arguments.getOptionalString("-authority");
        args = arguments.getRemainingArguments(0);
        if (indentation != null) {
            Formattable.setIndentation(indentation.intValue());
        }
        final BufferedReader in = new BufferedReader(Arguments.getReader(System.in));
        try {
            final Parser parser = new Parser();
            if (authority != null) {
                parser.setAuthority(Citations.fromName(authority));
            }
            parser.reformat(in, arguments.out, arguments.err);
        } catch (Exception exception) {
            exception.printStackTrace(arguments.err);
        }
        // Do not close 'in', since it is the standard input stream.
    }
}
