/******************************************************************************
 *
 * Project:  OpenGIS Simple Features Reference Implementation
 * Purpose:  OGRSpatialReference interface to OGC XML (014r4).
 * Author:   Frank Warmerdam, warmerdam@pobox.com
 *
 ******************************************************************************
 * Copyright (c) 2001, Frank Warmerdam (warmerdam@pobox.com)
 * Copyright (c) 2008-2012, Even Rouault <even dot rouault at spatialys.com>
 *
 * SPDX-License-Identifier: MIT
 ****************************************************************************/

#include "cpl_port.h"
#include "ogr_srs_api.h"

#include <cstdio>
#include <cstdlib>
#include <cstring>

#include "cpl_conv.h"
#include "cpl_error.h"
#include "cpl_minixml.h"
#include "cpl_multiproc.h"
#include "cpl_string.h"
#include "ogr_core.h"
#include "ogr_p.h"
#include "ogr_spatialref.h"

/************************************************************************/
/*                              parseURN()                              */
/*                                                                      */
/*      Parses requested sections out of URN.  The passed in URN        */
/*      *is* altered but the returned values point into the             */
/*      original string.                                                */
/************************************************************************/

static bool parseURN(char *pszURN, const char **ppszObjectType,
                     const char **ppszAuthority, const char **ppszCode,
                     const char **ppszVersion = nullptr)

{
    if (ppszObjectType != nullptr)
        *ppszObjectType = "";
    if (ppszAuthority != nullptr)
        *ppszAuthority = "";
    if (ppszCode != nullptr)
        *ppszCode = "";
    if (ppszVersion != nullptr)
        *ppszVersion = "";

    /* -------------------------------------------------------------------- */
    /*      Verify prefix.                                                  */
    /* -------------------------------------------------------------------- */
    if (!STARTS_WITH_CI(pszURN, "urn:ogc:def:"))
        return false;

    /* -------------------------------------------------------------------- */
    /*      Extract object type                                             */
    /* -------------------------------------------------------------------- */
    if (ppszObjectType != nullptr)
        *ppszObjectType = pszURN + 12;

    int i = 12;
    while (pszURN[i] != ':' && pszURN[i] != '\0')
        i++;

    if (pszURN[i] == '\0')
        return false;

    pszURN[i] = '\0';
    i++;

    /* -------------------------------------------------------------------- */
    /*      Extract authority                                               */
    /* -------------------------------------------------------------------- */
    if (ppszAuthority != nullptr)
        *ppszAuthority = pszURN + i;

    while (pszURN[i] != ':' && pszURN[i] != '\0')
        i++;

    if (pszURN[i] == '\0')
        return false;

    pszURN[i] = '\0';
    i++;

    /* -------------------------------------------------------------------- */
    /*      Extract version                                                 */
    /* -------------------------------------------------------------------- */
    if (ppszVersion != nullptr)
        *ppszVersion = pszURN + i;

    while (pszURN[i] != ':' && pszURN[i] != '\0')
        i++;

    if (pszURN[i] == '\0')
        return false;

    pszURN[i] = '\0';
    i++;

    /* -------------------------------------------------------------------- */
    /*      Extract code.                                                   */
    /* -------------------------------------------------------------------- */
    if (ppszCode != nullptr)
        *ppszCode = pszURN + i;

    return true;
}

/************************************************************************/
/*                               addURN()                               */
/************************************************************************/

static void addURN(CPLXMLNode *psTarget, const char *pszAuthority,
                   const char *pszObjectType, int nCode,
                   const char *pszVersion = "")

{
    if (pszVersion == nullptr)
        pszVersion = "";

    std::string osURN("urn:ogc:def:");
    osURN += pszObjectType;
    osURN += ':';
    osURN += pszAuthority;
    osURN += ':';
    osURN += pszVersion;
    osURN += ':';

    if (nCode != 0)
        osURN.append(std::to_string(nCode));

    CPLCreateXMLNode(CPLCreateXMLNode(psTarget, CXT_Attribute, "xlink:href"),
                     CXT_Text, osURN.c_str());
}

/************************************************************************/
/*                         AddValueIDWithURN()                          */
/*                                                                      */
/*      Adds element of the form <ElementName                           */
/*      xlink:href="urn_without_id">id</ElementName>"                   */
/************************************************************************/

static CPLXMLNode *AddValueIDWithURN(CPLXMLNode *psTarget,
                                     const char *pszElement,
                                     const char *pszAuthority,
                                     const char *pszObjectType, int nCode,
                                     const char *pszVersion = "")

{
    CPLXMLNode *psElement = CPLCreateXMLNode(psTarget, CXT_Element, pszElement);
    addURN(psElement, pszAuthority, pszObjectType, nCode, pszVersion);

    return psElement;
}

/************************************************************************/
/*                          addAuthorityIDBlock()                          */
/*                                                                      */
/*      Creates a structure like:                                       */
/*      <srsId>                                                         */
/*        <name codeSpace="urn">code</name>                             */
/*      </srsId>                                                        */
/************************************************************************/
static CPLXMLNode *addAuthorityIDBlock(CPLXMLNode *psTarget,
                                       const char *pszElement,
                                       const char *pszAuthority,
                                       const char *pszObjectType, int nCode,
                                       const char *pszVersion = "")

{
    /* -------------------------------------------------------------------- */
    /*      Prepare partial URN without the actual code.                    */
    /* -------------------------------------------------------------------- */
    if (pszVersion == nullptr)
        pszVersion = "";

    std::string osURN("urn:ogc:def:");
    osURN += pszObjectType;
    osURN += ':';
    osURN += pszAuthority;
    osURN += ':';
    osURN += pszVersion;
    osURN += ':';

    /* -------------------------------------------------------------------- */
    /*      Prepare the base name, eg. <srsID>.                             */
    /* -------------------------------------------------------------------- */
    CPLXMLNode *psElement = CPLCreateXMLNode(psTarget, CXT_Element, pszElement);

    /* -------------------------------------------------------------------- */
    /*      Prepare the name element.                                       */
    /* -------------------------------------------------------------------- */
    CPLXMLNode *psName = CPLCreateXMLNode(psElement, CXT_Element, "gml:name");

    /* -------------------------------------------------------------------- */
    /*      Prepare the codespace attribute.                                */
    /* -------------------------------------------------------------------- */
    CPLCreateXMLNode(CPLCreateXMLNode(psName, CXT_Attribute, "codeSpace"),
                     CXT_Text, osURN.c_str());

    /* -------------------------------------------------------------------- */
    /*      Attach code value to name node.                                 */
    /* -------------------------------------------------------------------- */
    char szCode[32] = {};
    snprintf(szCode, sizeof(szCode), "%d", nCode);

    CPLCreateXMLNode(psName, CXT_Text, szCode);

    return psElement;
}

/************************************************************************/
/*                              addGMLId()                              */
/************************************************************************/

static void addGMLId(CPLXMLNode *psParent)
{
    static CPLMutex *hGMLIdMutex = nullptr;
    CPLMutexHolderD(&hGMLIdMutex);

    static int nNextGMLId = 1;
    char szIdText[40] = {};

    snprintf(szIdText, sizeof(szIdText), "ogrcrs%d", nNextGMLId++);

    CPLCreateXMLNode(CPLCreateXMLNode(psParent, CXT_Attribute, "gml:id"),
                     CXT_Text, szIdText);
}

/************************************************************************/
/*                        exportAuthorityToXML()                        */
/************************************************************************/

static CPLXMLNode *exportAuthorityToXML(const OGR_SRSNode *poAuthParent,
                                        const char *pszTagName,
                                        CPLXMLNode *psXMLParent,
                                        const char *pszObjectType,
                                        int bUseSubName = TRUE)

{
    /* -------------------------------------------------------------------- */
    /*      Get authority node from parent.                                 */
    /* -------------------------------------------------------------------- */
    const int nAuthority = poAuthParent->FindChild("AUTHORITY");
    if (nAuthority == -1)
        return nullptr;

    const OGR_SRSNode *poAuthority = poAuthParent->GetChild(nAuthority);

    /* -------------------------------------------------------------------- */
    /*      Create identification.                                          */
    /* -------------------------------------------------------------------- */
    if (poAuthority->GetChildCount() < 2)
        return nullptr;

    const char *pszCodeSpace = poAuthority->GetChild(0)->GetValue();
    const char *pszCode = poAuthority->GetChild(1)->GetValue();
    const char *pszEdition = nullptr;

    if (bUseSubName)
        return addAuthorityIDBlock(psXMLParent, pszTagName, pszCodeSpace,
                                   pszObjectType, atoi(pszCode), pszEdition);

    return AddValueIDWithURN(psXMLParent, pszTagName, pszCodeSpace,
                             pszObjectType, atoi(pszCode), pszEdition);
}

/************************************************************************/
/*                             addProjArg()                             */
/************************************************************************/

static void addProjArg(const OGRSpatialReference *poSRS, CPLXMLNode *psBase,
                       const char *pszMeasureType, double dfDefault,
                       int nParameterID, const char *pszWKTName)

{
    CPLXMLNode *psNode = CPLCreateXMLNode(psBase, CXT_Element, "gml:usesValue");

    /* -------------------------------------------------------------------- */
    /*      Handle the UOM.                                                 */
    /* -------------------------------------------------------------------- */
    const char *pszUOMValue = EQUAL(pszMeasureType, "Angular")
                                  ? "urn:ogc:def:uom:EPSG::9102"
                                  : "urn:ogc:def:uom:EPSG::9001";

    CPLXMLNode *psValue = CPLCreateXMLNode(psNode, CXT_Element, "gml:value");

    CPLCreateXMLNode(CPLCreateXMLNode(psValue, CXT_Attribute, "uom"), CXT_Text,
                     pszUOMValue);

    /* -------------------------------------------------------------------- */
    /*      Add the parameter value itself.                                 */
    /* -------------------------------------------------------------------- */
    double dfParamValue =
        poSRS->GetNormProjParm(pszWKTName, dfDefault, nullptr);

    CPLCreateXMLNode(psValue, CXT_Text,
                     CPLString().Printf("%.16g", dfParamValue));

    /* -------------------------------------------------------------------- */
    /*      Add the valueOfParameter.                                       */
    /* -------------------------------------------------------------------- */
    AddValueIDWithURN(psNode, "gml:valueOfParameter", "EPSG", "parameter",
                      nParameterID);
}

/************************************************************************/
/*                              addAxis()                               */
/*                                                                      */
/*      Added the <usesAxis> element and down.                          */
/************************************************************************/

static CPLXMLNode *addAxis(CPLXMLNode *psXMLParent,
                           const char *pszAxis,  // "Lat", "Long", "E" or "N"
                           const OGR_SRSNode * /* poUnitsSrc */)

{
    CPLXMLNode *psAxisXML = CPLCreateXMLNode(
        CPLCreateXMLNode(psXMLParent, CXT_Element, "gml:usesAxis"), CXT_Element,
        "gml:CoordinateSystemAxis");
    if (!psAxisXML)
    {
        CPLError(CE_Failure, CPLE_AppDefined, "addAxis failed.");
        return nullptr;
    }
    addGMLId(psAxisXML);

    if (EQUAL(pszAxis, "Lat"))
    {
        CPLCreateXMLNode(CPLCreateXMLNode(psAxisXML, CXT_Attribute, "gml:uom"),
                         CXT_Text, "urn:ogc:def:uom:EPSG::9102");

        CPLCreateXMLElementAndValue(psAxisXML, "gml:name", "Geodetic latitude");
        addAuthorityIDBlock(psAxisXML, "gml:axisID", "EPSG", "axis", 9901);
        CPLCreateXMLElementAndValue(psAxisXML, "gml:axisAbbrev", "Lat");
        CPLCreateXMLElementAndValue(psAxisXML, "gml:axisDirection", "north");
    }
    else if (EQUAL(pszAxis, "Long"))
    {
        CPLCreateXMLNode(CPLCreateXMLNode(psAxisXML, CXT_Attribute, "gml:uom"),
                         CXT_Text, "urn:ogc:def:uom:EPSG::9102");

        CPLCreateXMLElementAndValue(psAxisXML, "gml:name",
                                    "Geodetic longitude");
        addAuthorityIDBlock(psAxisXML, "gml:axisID", "EPSG", "axis", 9902);
        CPLCreateXMLElementAndValue(psAxisXML, "gml:axisAbbrev", "Lon");
        CPLCreateXMLElementAndValue(psAxisXML, "gml:axisDirection", "east");
    }
    else if (EQUAL(pszAxis, "E"))
    {
        CPLCreateXMLNode(CPLCreateXMLNode(psAxisXML, CXT_Attribute, "gml:uom"),
                         CXT_Text, "urn:ogc:def:uom:EPSG::9001");

        CPLCreateXMLElementAndValue(psAxisXML, "gml:name", "Easting");
        addAuthorityIDBlock(psAxisXML, "gml:axisID", "EPSG", "axis", 9906);
        CPLCreateXMLElementAndValue(psAxisXML, "gml:axisAbbrev", "E");
        CPLCreateXMLElementAndValue(psAxisXML, "gml:axisDirection", "east");
    }
    else if (EQUAL(pszAxis, "N"))
    {
        CPLCreateXMLNode(CPLCreateXMLNode(psAxisXML, CXT_Attribute, "gml:uom"),
                         CXT_Text, "urn:ogc:def:uom:EPSG::9001");

        CPLCreateXMLElementAndValue(psAxisXML, "gml:name", "Northing");
        addAuthorityIDBlock(psAxisXML, "gml:axisID", "EPSG", "axis", 9907);
        CPLCreateXMLElementAndValue(psAxisXML, "gml:axisAbbrev", "N");
        CPLCreateXMLElementAndValue(psAxisXML, "gml:axisDirection", "north");
    }
    else
    {
        CPLAssert(false);
    }

    return psAxisXML;
}

/************************************************************************/
/*                         exportGeogCSToXML()                          */
/************************************************************************/

static CPLXMLNode *exportGeogCSToXML(const OGRSpatialReference *poSRS)

{
    const OGR_SRSNode *poGeogCS = poSRS->GetAttrNode("GEOGCS");

    if (poGeogCS == nullptr)
        return nullptr;

    /* -------------------------------------------------------------------- */
    /*      Establish initial infrastructure.                               */
    /* -------------------------------------------------------------------- */
    CPLXMLNode *psGCS_XML =
        CPLCreateXMLNode(nullptr, CXT_Element, "gml:GeographicCRS");
    addGMLId(psGCS_XML);

    /* -------------------------------------------------------------------- */
    /*      Attach symbolic name (srsName).                                 */
    /* -------------------------------------------------------------------- */
    CPLCreateXMLElementAndValue(psGCS_XML, "gml:srsName",
                                poGeogCS->GetChild(0)->GetValue());

    /* -------------------------------------------------------------------- */
    /*      Does the overall coordinate system have an authority?  If so    */
    /*      attach as an identification section.                            */
    /* -------------------------------------------------------------------- */
    exportAuthorityToXML(poGeogCS, "gml:srsID", psGCS_XML, "crs");

    /* -------------------------------------------------------------------- */
    /*      Insert a big whack of fixed stuff defining the                  */
    /*      ellipsoidalCS.  Basically this defines the axes and their       */
    /*      units.                                                          */
    /* -------------------------------------------------------------------- */
    CPLXMLNode *psECS = CPLCreateXMLNode(
        CPLCreateXMLNode(psGCS_XML, CXT_Element, "gml:usesEllipsoidalCS"),
        CXT_Element, "gml:EllipsoidalCS");

    addGMLId(psECS);

    CPLCreateXMLElementAndValue(psECS, "gml:csName", "ellipsoidal");

    addAuthorityIDBlock(psECS, "gml:csID", "EPSG", "cs", 6402);

    addAxis(psECS, "Lat", nullptr);
    addAxis(psECS, "Long", nullptr);

    /* -------------------------------------------------------------------- */
    /*      Start with the datum.                                           */
    /* -------------------------------------------------------------------- */
    const OGR_SRSNode *poDatum = poGeogCS->GetNode("DATUM");

    if (poDatum == nullptr)
    {
        CPLDestroyXMLNode(psGCS_XML);
        return nullptr;
    }

    CPLXMLNode *psDatumXML = CPLCreateXMLNode(
        CPLCreateXMLNode(psGCS_XML, CXT_Element, "gml:usesGeodeticDatum"),
        CXT_Element, "gml:GeodeticDatum");

    addGMLId(psDatumXML);

    /* -------------------------------------------------------------------- */
    /*      Set the datumName.                                              */
    /* -------------------------------------------------------------------- */
    CPLCreateXMLElementAndValue(psDatumXML, "gml:datumName",
                                poDatum->GetChild(0)->GetValue());

    /* -------------------------------------------------------------------- */
    /*      Set authority id info if available.                             */
    /* -------------------------------------------------------------------- */
    exportAuthorityToXML(poDatum, "gml:datumID", psDatumXML, "datum");

    /* -------------------------------------------------------------------- */
    /*      Setup prime meridian information.                               */
    /* -------------------------------------------------------------------- */
    const OGR_SRSNode *poPMNode = poGeogCS->GetNode("PRIMEM");
    const char *pszPMName = "Greenwich";
    double dfPMOffset = poSRS->GetPrimeMeridian(&pszPMName);

    CPLXMLNode *psPM = CPLCreateXMLNode(
        CPLCreateXMLNode(psDatumXML, CXT_Element, "gml:usesPrimeMeridian"),
        CXT_Element, "gml:PrimeMeridian");

    addGMLId(psPM);

    CPLCreateXMLElementAndValue(psPM, "gml:meridianName", pszPMName);

    if (poPMNode)
        exportAuthorityToXML(poPMNode, "gml:meridianID", psPM, "meridian");

    CPLXMLNode *psAngle = CPLCreateXMLNode(
        CPLCreateXMLNode(psPM, CXT_Element, "gml:greenwichLongitude"),
        CXT_Element, "gml:angle");

    CPLCreateXMLNode(CPLCreateXMLNode(psAngle, CXT_Attribute, "uom"), CXT_Text,
                     "urn:ogc:def:uom:EPSG::9102");

    CPLCreateXMLNode(psAngle, CXT_Text,
                     CPLString().Printf("%.16g", dfPMOffset));

    /* -------------------------------------------------------------------- */
    /*      Translate the ellipsoid.                                        */
    /* -------------------------------------------------------------------- */
    const OGR_SRSNode *poEllipsoid = poDatum->GetNode("SPHEROID");

    if (poEllipsoid != nullptr)
    {
        CPLXMLNode *psEllipseXML = CPLCreateXMLNode(
            CPLCreateXMLNode(psDatumXML, CXT_Element, "gml:usesEllipsoid"),
            CXT_Element, "gml:Ellipsoid");

        addGMLId(psEllipseXML);

        CPLCreateXMLElementAndValue(psEllipseXML, "gml:ellipsoidName",
                                    poEllipsoid->GetChild(0)->GetValue());

        exportAuthorityToXML(poEllipsoid, "gml:ellipsoidID", psEllipseXML,
                             "ellipsoid");

        CPLXMLNode *psParamXML =
            CPLCreateXMLNode(psEllipseXML, CXT_Element, "gml:semiMajorAxis");

        CPLCreateXMLNode(CPLCreateXMLNode(psParamXML, CXT_Attribute, "uom"),
                         CXT_Text, "urn:ogc:def:uom:EPSG::9001");

        CPLCreateXMLNode(psParamXML, CXT_Text,
                         poEllipsoid->GetChild(1)->GetValue());

        psParamXML =
            CPLCreateXMLNode(CPLCreateXMLNode(psEllipseXML, CXT_Element,
                                              "gml:secondDefiningParameter"),
                             CXT_Element, "gml:inverseFlattening");

        CPLCreateXMLNode(CPLCreateXMLNode(psParamXML, CXT_Attribute, "uom"),
                         CXT_Text, "urn:ogc:def:uom:EPSG::9201");

        CPLCreateXMLNode(psParamXML, CXT_Text,
                         poEllipsoid->GetChild(2)->GetValue());
    }

    return psGCS_XML;
}

/************************************************************************/
/*                         exportProjCSToXML()                          */
/************************************************************************/

static CPLXMLNode *exportProjCSToXML(const OGRSpatialReference *poSRS)

{
    const OGR_SRSNode *poProjCS = poSRS->GetAttrNode("PROJCS");

    if (poProjCS == nullptr)
        return nullptr;

    /* -------------------------------------------------------------------- */
    /*      Establish initial infrastructure.                               */
    /* -------------------------------------------------------------------- */
    CPLXMLNode *psCRS_XML =
        CPLCreateXMLNode(nullptr, CXT_Element, "gml:ProjectedCRS");
    addGMLId(psCRS_XML);

    /* -------------------------------------------------------------------- */
    /*      Attach symbolic name (a name in a nameset).                     */
    /* -------------------------------------------------------------------- */
    CPLCreateXMLElementAndValue(psCRS_XML, "gml:srsName",
                                poProjCS->GetChild(0)->GetValue());

    /* -------------------------------------------------------------------- */
    /*      Add authority info if we have it.                               */
    /* -------------------------------------------------------------------- */
    exportAuthorityToXML(poProjCS, "gml:srsID", psCRS_XML, "crs");

    /* -------------------------------------------------------------------- */
    /*      Use the GEOGCS as a <baseCRS>                                   */
    /* -------------------------------------------------------------------- */
    CPLXMLNode *psBaseCRSXML =
        CPLCreateXMLNode(psCRS_XML, CXT_Element, "gml:baseCRS");

    CPLAddXMLChild(psBaseCRSXML, exportGeogCSToXML(poSRS));

    /* -------------------------------------------------------------------- */
    /*      Our projected coordinate system is "defined by Conversion".     */
    /* -------------------------------------------------------------------- */
    CPLXMLNode *psDefinedBy =
        CPLCreateXMLNode(psCRS_XML, CXT_Element, "gml:definedByConversion");

    /* -------------------------------------------------------------------- */
    /*      Projections are handled as ParameterizedTransformations.        */
    /* -------------------------------------------------------------------- */
    const char *pszProjection = poSRS->GetAttrValue("PROJECTION");
    CPLXMLNode *psConv =
        CPLCreateXMLNode(psDefinedBy, CXT_Element, "gml:Conversion");
    addGMLId(psConv);

    CPLCreateXMLNode(
        CPLCreateXMLNode(psConv, CXT_Element, "gml:coordinateOperationName"),
        CXT_Text, pszProjection);

    /* -------------------------------------------------------------------- */
    /*      Transverse Mercator                                             */
    /* -------------------------------------------------------------------- */
    if (pszProjection == nullptr)
    {
        CPLError(CE_Failure, CPLE_NotSupported, "No projection method");
    }
    else if (EQUAL(pszProjection, SRS_PT_TRANSVERSE_MERCATOR))
    {
        AddValueIDWithURN(psConv, "gml:usesMethod", "EPSG", "method", 9807);

        addProjArg(poSRS, psConv, "Angular", 0.0, 8801,
                   SRS_PP_LATITUDE_OF_ORIGIN);
        addProjArg(poSRS, psConv, "Angular", 0.0, 8802,
                   SRS_PP_CENTRAL_MERIDIAN);
        addProjArg(poSRS, psConv, "Unitless", 1.0, 8805, SRS_PP_SCALE_FACTOR);
        addProjArg(poSRS, psConv, "Linear", 0.0, 8806, SRS_PP_FALSE_EASTING);
        addProjArg(poSRS, psConv, "Linear", 0.0, 8807, SRS_PP_FALSE_NORTHING);
    }
    /* -------------------------------------------------------------------- */
    /*      Lambert Conformal Conic                                         */
    /* -------------------------------------------------------------------- */
    else if (EQUAL(pszProjection, SRS_PT_LAMBERT_CONFORMAL_CONIC_1SP))
    {
        AddValueIDWithURN(psConv, "gml:usesMethod", "EPSG", "method", 9801);

        addProjArg(poSRS, psConv, "Angular", 0.0, 8801,
                   SRS_PP_LATITUDE_OF_ORIGIN);
        addProjArg(poSRS, psConv, "Angular", 0.0, 8802,
                   SRS_PP_CENTRAL_MERIDIAN);
        addProjArg(poSRS, psConv, "Unitless", 1.0, 8805, SRS_PP_SCALE_FACTOR);
        addProjArg(poSRS, psConv, "Linear", 0.0, 8806, SRS_PP_FALSE_EASTING);
        addProjArg(poSRS, psConv, "Linear", 0.0, 8807, SRS_PP_FALSE_NORTHING);
    }
    else
    {
        CPLError(CE_Failure, CPLE_NotSupported,
                 "Unhandled projection method %s", pszProjection);
        CPLDestroyXMLNode(psCRS_XML);
        return nullptr;
    }

    /* -------------------------------------------------------------------- */
    /*      Define the cartesian coordinate system.                         */
    /* -------------------------------------------------------------------- */
    CPLXMLNode *psCCS = CPLCreateXMLNode(
        CPLCreateXMLNode(psCRS_XML, CXT_Element, "gml:usesCartesianCS"),
        CXT_Element, "gml:CartesianCS");

    addGMLId(psCCS);

    CPLCreateXMLElementAndValue(psCCS, "gml:csName", "Cartesian");
    addAuthorityIDBlock(psCCS, "gml:csID", "EPSG", "cs", 4400);
    addAxis(psCCS, "E", nullptr);
    addAxis(psCCS, "N", nullptr);

    return psCRS_XML;
}

/************************************************************************/
/*                            exportToXML()                             */
/************************************************************************/

/**
 * \brief Export coordinate system in XML format.
 *
 * Converts the loaded coordinate reference system into XML format
 * to the extent possible.  The string returned in ppszRawXML should be
 * deallocated by the caller with CPLFree() when no longer needed.
 *
 * LOCAL_CS coordinate systems are not translatable.  An empty string
 * will be returned along with OGRERR_NONE.
 *
 * This method is the equivalent of the C function OSRExportToXML().
 *
 * @param ppszRawXML pointer to which dynamically allocated XML definition
 * will be assigned.
 * @param pszDialect currently ignored. The dialect used is GML based.
 *
 * @return OGRERR_NONE on success or an error code on failure.
 */

OGRErr OGRSpatialReference::exportToXML(char **ppszRawXML,
                                        CPL_UNUSED const char *pszDialect) const
{
    CPLXMLNode *psXMLTree = nullptr;

    if (IsGeographic())
    {
        psXMLTree = exportGeogCSToXML(this);
    }
    else if (IsProjected())
    {
        psXMLTree = exportProjCSToXML(this);
    }
    else
        return OGRERR_UNSUPPORTED_SRS;

    if (!psXMLTree)
        return OGRERR_FAILURE;

    *ppszRawXML = CPLSerializeXMLTree(psXMLTree);
    CPLDestroyXMLNode(psXMLTree);

    return OGRERR_NONE;
}

/************************************************************************/
/*                           OSRExportToXML()                           */
/************************************************************************/
/**
 * \brief Export coordinate system in XML format.
 *
 * This function is the same as OGRSpatialReference::exportToXML().
 */

OGRErr OSRExportToXML(OGRSpatialReferenceH hSRS, char **ppszRawXML,
                      const char *pszDialect)

{
    VALIDATE_POINTER1(hSRS, "OSRExportToXML", OGRERR_FAILURE);

    return OGRSpatialReference::FromHandle(hSRS)->exportToXML(ppszRawXML,
                                                              pszDialect);
}

#ifdef notdef
/************************************************************************/
/*                           importXMLUnits()                           */
/************************************************************************/

static void importXMLUnits(CPLXMLNode *psSrcXML, const char *pszClass,
                           OGRSpatialReference *poSRS, const char *pszTarget)

{
    OGR_SRSNode *poNode = poSRS->GetAttrNode(pszTarget);

    CPLAssert(EQUAL(pszClass, "AngularUnit") || EQUAL(pszClass, "LinearUnit"));

    psSrcXML = CPLGetXMLNode(psSrcXML, pszClass);

    OGR_SRSNode *poUnits = NULL;
    const char *pszUnitName = NULL;
    const char *pszUnitsPer = NULL;

    // TODO(schwehr): Remove the goto.
    if (psSrcXML == NULL)
        goto DefaultTarget;

    pszUnitName = CPLGetXMLValue(psSrcXML, "NameSet.name", "unnamed");

    pszUnitsPer = EQUAL(pszClass, "AngularUnit")
                      ? CPLGetXMLValue(psSrcXML, "radiansPerUnit", NULL)
                      : CPLGetXMLValue(psSrcXML, "metresPerUnit", NULL);

    if (pszUnitsPer == NULL)
    {
        CPLDebug("OGR_SRS_XML", "Missing PerUnit value for %s.", pszClass);
        goto DefaultTarget;
    }

    if (poNode == NULL)
    {
        CPLDebug("OGR_SRS_XML", "Can't find %s in importXMLUnits.", pszTarget);
        goto DefaultTarget;
    }

    if (poNode->FindChild("UNIT") != -1)
    {
        poUnits = poNode->GetChild(poNode->FindChild("UNIT"));
        poUnits->GetChild(0)->SetValue(pszUnitName);
        poUnits->GetChild(1)->SetValue(pszUnitsPer);
    }
    else
    {
        poUnits = new OGR_SRSNode("UNIT");
        poUnits->AddChild(new OGR_SRSNode(pszUnitName));
        poUnits->AddChild(new OGR_SRSNode(pszUnitsPer));

        poNode->AddChild(poUnits);
    }
    return;

DefaultTarget:
    poUnits = new OGR_SRSNode("UNIT");
    if (EQUAL(pszClass, "AngularUnit"))
    {
        poUnits->AddChild(new OGR_SRSNode(SRS_UA_DEGREE));
        poUnits->AddChild(new OGR_SRSNode(SRS_UA_DEGREE_CONV));
    }
    else
    {
        poUnits->AddChild(new OGR_SRSNode(SRS_UL_METER));
        poUnits->AddChild(new OGR_SRSNode("1.0"));
    }

    poNode->AddChild(poUnits);
}
#endif

/************************************************************************/
/*                         importXMLAuthority()                         */
/************************************************************************/

static void importXMLAuthority(CPLXMLNode *psSrcXML, OGRSpatialReference *poSRS,
                               const char *pszSourceKey,
                               const char *pszTargetKey)

{
    CPLXMLNode *psIDNode = CPLGetXMLNode(psSrcXML, pszSourceKey);
    CPLXMLNode *psNameNode = CPLGetXMLNode(psIDNode, "name");
    CPLXMLNode *psCodeSpace = CPLGetXMLNode(psNameNode, "codeSpace");

    if (psIDNode == nullptr || psNameNode == nullptr || psCodeSpace == nullptr)
        return;

    char *pszURN = CPLStrdup(CPLGetXMLValue(psCodeSpace, "", ""));

    const char *pszAuthority;
    const char *pszCode;
    if (!parseURN(pszURN, nullptr, &pszAuthority, &pszCode))
    {
        CPLFree(pszURN);
        return;
    }

    if (strlen(pszCode) == 0)
        pszCode = CPLGetXMLValue(psNameNode, "", "");

    const int nCode = pszCode != nullptr ? atoi(pszCode) : 0;

    if (nCode != 0)
        poSRS->SetAuthority(pszTargetKey, pszAuthority, nCode);

    CPLFree(pszURN);
}

/************************************************************************/
/*                           ParseOGCDefURN()                           */
/*                                                                      */
/*      Parse out fields from a URN of the form:                        */
/*        urn:ogc:def:parameter:EPSG:6.3:9707                           */
/************************************************************************/

static bool ParseOGCDefURN(const char *pszURN, CPLString *poObjectType,
                           CPLString *poAuthority, CPLString *poVersion,
                           CPLString *poValue)

{
    if (poObjectType != nullptr)
        *poObjectType = "";

    if (poAuthority != nullptr)
        *poAuthority = "";

    if (poVersion != nullptr)
        *poVersion = "";

    if (poValue != nullptr)
        *poValue = "";

    if (pszURN == nullptr || !STARTS_WITH_CI(pszURN, "urn:ogc:def:"))
        return false;

    char **papszTokens =
        CSLTokenizeStringComplex(pszURN + 12, ":", FALSE, TRUE);

    if (CSLCount(papszTokens) != 4)
    {
        CSLDestroy(papszTokens);
        return false;
    }

    if (poObjectType != nullptr)
        *poObjectType = papszTokens[0];

    if (poAuthority != nullptr)
        *poAuthority = papszTokens[1];

    if (poVersion != nullptr)
        *poVersion = papszTokens[2];

    if (poValue != nullptr)
        *poValue = papszTokens[3];

    CSLDestroy(papszTokens);
    return true;
}

/************************************************************************/
/*                       getEPSGObjectCodeValue()                       */
/*                                                                      */
/*      Fetch a code value from the indicated node.  Should work on     */
/*      something of the form <elem xlink:href="urn:...:n" /> or        */
/*      something of the form <elem xlink:href="urn:...:">n</a>.        */
/************************************************************************/

static int getEPSGObjectCodeValue(CPLXMLNode *psNode,
                                  const char *pszEPSGObjectType, /*"method" */
                                  int nDefault)

{
    if (psNode == nullptr)
        return nDefault;

    const char *pszHrefVal = CPLGetXMLValue(psNode, "xlink:href", nullptr);
    if (pszHrefVal == nullptr)
        pszHrefVal = CPLGetXMLValue(psNode, "href", nullptr);

    CPLString osObjectType;
    CPLString osAuthority;
    CPLString osValue;
    if (!ParseOGCDefURN(pszHrefVal, &osObjectType, &osAuthority, nullptr,
                        &osValue))
        return nDefault;

    if (!EQUAL(osAuthority, "EPSG") || !EQUAL(osObjectType, pszEPSGObjectType))
        return nDefault;

    if (!osValue.empty())
        return atoi(osValue);

    const char *pszValue = CPLGetXMLValue(psNode, "", nullptr);
    if (pszValue != nullptr)
        return atoi(pszValue);

    return nDefault;
}

/************************************************************************/
/*                         getProjectionParam()                         */
/************************************************************************/

static double getProjectionParam(CPLXMLNode *psRootNode, int nParameterCode,
                                 const char * /*pszMeasureType */,
                                 double dfDefault)

{
    for (CPLXMLNode *psUsesParameter = psRootNode->psChild;
         psUsesParameter != nullptr; psUsesParameter = psUsesParameter->psNext)
    {
        if (psUsesParameter->eType != CXT_Element)
            continue;

        if (!EQUAL(psUsesParameter->pszValue, "usesParameterValue") &&
            !EQUAL(psUsesParameter->pszValue, "usesValue"))
            continue;

        if (getEPSGObjectCodeValue(
                CPLGetXMLNode(psUsesParameter, "valueOfParameter"), "parameter",
                0) == nParameterCode)
        {
            const char *pszValue =
                CPLGetXMLValue(psUsesParameter, "value", nullptr);

            if (pszValue == nullptr)
                return dfDefault;

            return CPLAtof(pszValue);
        }
    }

    return dfDefault;
}

/************************************************************************/
/*                         getNormalizedValue()                         */
/*                                                                      */
/*      Parse a node to get its numerical value, and then normalize     */
/*      into meters of degrees depending on the measure type.           */
/************************************************************************/

static double getNormalizedValue(CPLXMLNode *psNode, const char *pszPath,
                                 const char * /*pszMeasure*/, double dfDefault)

{
    CPLXMLNode *psTargetNode = pszPath == nullptr || strlen(pszPath) == 0
                                   ? psNode
                                   : CPLGetXMLNode(psNode, pszPath);

    if (psTargetNode == nullptr)
        return dfDefault;

    CPLXMLNode *psValueNode = psTargetNode->psChild;  // Used after for.
    for (; psValueNode != nullptr && psValueNode->eType != CXT_Text;
         psValueNode = psValueNode->psNext)
    {
    }

    if (psValueNode == nullptr)
        return dfDefault;

    // Add normalization later.

    return CPLAtof(psValueNode->pszValue);
}

/************************************************************************/
/*                        importGeogCSFromXML()                         */
/************************************************************************/

static OGRErr importGeogCSFromXML(OGRSpatialReference *poSRS, CPLXMLNode *psCRS)

{
    /* -------------------------------------------------------------------- */
    /*      Set the GEOGCS name from the srsName.                           */
    /* -------------------------------------------------------------------- */
    const char *pszGeogName =
        CPLGetXMLValue(psCRS, "srsName", "Unnamed GeogCS");

    /* -------------------------------------------------------------------- */
    /*      If we don't seem to have a detailed coordinate system           */
    /*      definition, check if we can define based on an EPSG code.       */
    /* -------------------------------------------------------------------- */
    CPLXMLNode *psDatum =
        CPLGetXMLNode(psCRS, "usesGeodeticDatum.GeodeticDatum");

    if (psDatum == nullptr)
    {
        OGRSpatialReference oIdSRS;

        oIdSRS.SetLocalCS("dummy");
        importXMLAuthority(psCRS, &oIdSRS, "srsID", "LOCAL_CS");

        if (oIdSRS.GetAuthorityCode("LOCAL_CS") != nullptr &&
            oIdSRS.GetAuthorityName("LOCAL_CS") != nullptr &&
            EQUAL(oIdSRS.GetAuthorityName("LOCAL_CS"), "EPSG"))
        {
            return poSRS->importFromEPSG(
                atoi(oIdSRS.GetAuthorityCode("LOCAL_CS")));
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Get datum name.                                                 */
    /* -------------------------------------------------------------------- */
    const char *pszDatumName =
        CPLGetXMLValue(psDatum, "datumName", "Unnamed Datum");

    /* -------------------------------------------------------------------- */
    /*      Get ellipsoid information.                                      */
    /* -------------------------------------------------------------------- */
    CPLXMLNode *psE = CPLGetXMLNode(psDatum, "usesEllipsoid.Ellipsoid");
    const char *pszEllipsoidName =
        CPLGetXMLValue(psE, "ellipsoidName", "Unnamed Ellipsoid");

    const double dfSemiMajor =
        getNormalizedValue(psE, "semiMajorAxis", "Linear", SRS_WGS84_SEMIMAJOR);

    const double dfInvFlattening = getNormalizedValue(
        psE, "secondDefiningParameter.inverseFlattening", "Unitless", 0.0);

    if (dfInvFlattening == 0.0)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Ellipsoid inverseFlattening corrupt or missing.");
        return OGRERR_CORRUPT_DATA;
    }

    /* -------------------------------------------------------------------- */
    /*      Get the prime meridian.                                         */
    /* -------------------------------------------------------------------- */
    const char *pszPMName = nullptr;
    double dfPMOffset = 0.0;

    CPLXMLNode *psPM =
        CPLGetXMLNode(psDatum, "usesPrimeMeridian.PrimeMeridian");
    if (psPM == nullptr)
    {
        pszPMName = "Greenwich";
        dfPMOffset = 0.0;
    }
    else
    {
        pszPMName =
            CPLGetXMLValue(psPM, "meridianName", "Unnamed Prime Meridian");
        dfPMOffset = getNormalizedValue(psPM, "greenwichLongitude.angle",
                                        "Angular", 0.0);
    }

    /* -------------------------------------------------------------------- */
    /*      Set the geographic definition.                                  */
    /* -------------------------------------------------------------------- */
    poSRS->SetGeogCS(pszGeogName, pszDatumName, pszEllipsoidName, dfSemiMajor,
                     dfInvFlattening, pszPMName, dfPMOffset);

/* -------------------------------------------------------------------- */
/*      Look for angular units.  We don't check that all axes match     */
/*      at this time.                                                   */
/* -------------------------------------------------------------------- */
#if 0  // Does not compile.
    CPLXMLNode *psAxis =
        CPLGetXMLNode( psGeo2DCRS,
                       "EllipsoidalCoordinateSystem.CoordinateAxis" );
    importXMLUnits( psAxis, "AngularUnit", poSRS, "GEOGCS" );
#endif

    /* -------------------------------------------------------------------- */
    /*      Can we set authorities for any of the levels?                   */
    /* -------------------------------------------------------------------- */
    importXMLAuthority(psCRS, poSRS, "srsID", "GEOGCS");
    importXMLAuthority(psDatum, poSRS, "datumID", "GEOGCS|DATUM");
    importXMLAuthority(psE, poSRS, "ellipsoidID", "GEOGCS|DATUM|SPHEROID");
    importXMLAuthority(psDatum, poSRS,
                       "usesPrimeMeridian.PrimeMeridian.meridianID",
                       "GEOGCS|PRIMEM");

    return OGRERR_NONE;
}

/************************************************************************/
/*                        importProjCSFromXML()                         */
/************************************************************************/

static OGRErr importProjCSFromXML(OGRSpatialReference *poSRS, CPLXMLNode *psCRS)

{
    /* -------------------------------------------------------------------- */
    /*      Setup the PROJCS node with a name.                              */
    /* -------------------------------------------------------------------- */
    poSRS->SetProjCS(CPLGetXMLValue(psCRS, "srsName", "Unnamed"));

    /* -------------------------------------------------------------------- */
    /*      Get authority information if available.  If we got it, and      */
    /*      we seem to be lacking inline definition values, try and         */
    /*      define according to the EPSG code for the PCS.                  */
    /* -------------------------------------------------------------------- */
    importXMLAuthority(psCRS, poSRS, "srsID", "PROJCS");

    if (poSRS->GetAuthorityCode("PROJCS") != nullptr &&
        poSRS->GetAuthorityName("PROJCS") != nullptr &&
        EQUAL(poSRS->GetAuthorityName("PROJCS"), "EPSG") &&
        (CPLGetXMLNode(psCRS, "definedByConversion.Conversion") == nullptr ||
         CPLGetXMLNode(psCRS, "baseCRS.GeographicCRS") == nullptr))
    {
        return poSRS->importFromEPSG(atoi(poSRS->GetAuthorityCode("PROJCS")));
    }

    /* -------------------------------------------------------------------- */
    /*      Try to set the GEOGCS info.                                     */
    /* -------------------------------------------------------------------- */

    CPLXMLNode *psSubXML = CPLGetXMLNode(psCRS, "baseCRS.GeographicCRS");
    if (psSubXML != nullptr)
    {
        const OGRErr eErr = importGeogCSFromXML(poSRS, psSubXML);
        if (eErr != OGRERR_NONE)
            return eErr;
    }

    /* -------------------------------------------------------------------- */
    /*      Get the conversion node.  It should be the only child of the    */
    /*      definedByConversion node.                                       */
    /* -------------------------------------------------------------------- */
    CPLXMLNode *psConv = CPLGetXMLNode(psCRS, "definedByConversion.Conversion");
    if (psConv == nullptr || psConv->eType != CXT_Element)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Unable to find a conversion node under the "
                 "definedByConversion node of the ProjectedCRS.");
        return OGRERR_CORRUPT_DATA;
    }

    /* -------------------------------------------------------------------- */
    /*      Determine the conversion method in effect.                      */
    /* -------------------------------------------------------------------- */
    const int nMethod = getEPSGObjectCodeValue(
        CPLGetXMLNode(psConv, "usesMethod"), "method", 0);

    /* -------------------------------------------------------------------- */
    /*      Transverse Mercator.                                            */
    /* -------------------------------------------------------------------- */
    if (nMethod == 9807)
    {
        poSRS->SetTM(getProjectionParam(psConv, 8801, "Angular", 0.0),
                     getProjectionParam(psConv, 8802, "Angular", 0.0),
                     getProjectionParam(psConv, 8805, "Unitless", 1.0),
                     getProjectionParam(psConv, 8806, "Linear", 0.0),
                     getProjectionParam(psConv, 8807, "Linear", 0.0));
    }

    /* -------------------------------------------------------------------- */
    /*      Didn't recognise?                                               */
    /* -------------------------------------------------------------------- */
    else
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Conversion method %d not recognised.", nMethod);
        return OGRERR_CORRUPT_DATA;
    }

    // Re-set authority as all editions above will have removed it
    importXMLAuthority(psCRS, poSRS, "srsID", "PROJCS");

    // Need to get linear units here!

    return OGRERR_NONE;
}

/************************************************************************/
/*                           importFromXML()                            */
/************************************************************************/

/**
 * \brief Import coordinate system from XML format (GML only currently).
 *
 * This method is the same as the C function OSRImportFromXML()
 * @param pszXML XML string to import
 * @return OGRERR_NONE on success or OGRERR_CORRUPT_DATA on failure.
 */
OGRErr OGRSpatialReference::importFromXML(const char *pszXML)

{
    Clear();

    /* -------------------------------------------------------------------- */
    /*      Parse the XML.                                                  */
    /* -------------------------------------------------------------------- */
    CPLXMLNode *psTree = CPLParseXMLString(pszXML);

    if (psTree == nullptr)
        return OGRERR_CORRUPT_DATA;

    CPLStripXMLNamespace(psTree, "gml", TRUE);

    /* -------------------------------------------------------------------- */
    /*      Import according to the root node type.  We walk through        */
    /*      root elements as there is sometimes prefix stuff like           */
    /*      <?xml>.                                                         */
    /* -------------------------------------------------------------------- */
    OGRErr eErr = OGRERR_UNSUPPORTED_SRS;

    for (CPLXMLNode *psNode = psTree; psNode != nullptr;
         psNode = psNode->psNext)
    {
        if (EQUAL(psNode->pszValue, "GeographicCRS"))
        {
            eErr = importGeogCSFromXML(this, psNode);
            break;
        }

        else if (EQUAL(psNode->pszValue, "ProjectedCRS"))
        {
            eErr = importProjCSFromXML(this, psNode);
            break;
        }
    }

    CPLDestroyXMLNode(psTree);

    return eErr;
}

/************************************************************************/
/*                          OSRImportFromXML()                          */
/************************************************************************/

/**
 * \brief Import coordinate system from XML format (GML only currently).
 *
 * This function is the same as OGRSpatialReference::importFromXML().
 */
OGRErr OSRImportFromXML(OGRSpatialReferenceH hSRS, const char *pszXML)

{
    VALIDATE_POINTER1(hSRS, "OSRImportFromXML", OGRERR_FAILURE);
    VALIDATE_POINTER1(pszXML, "OSRImportFromXML", OGRERR_FAILURE);

    return OGRSpatialReference::FromHandle(hSRS)->importFromXML(pszXML);
}
