/******************************************************************************
 *
 * Project:  GML Reader
 * Purpose:  Implementation of GMLParseXSD()
 * Author:   Frank Warmerdam, warmerdam@pobox.com
 *
 ******************************************************************************
 * Copyright (c) 2005, Frank Warmerdam
 * Copyright (c) 2010-2014, Even Rouault <even dot rouault at spatialys.com>
 *
 * SPDX-License-Identifier: MIT
 ****************************************************************************/

#include "cpl_port.h"
#include "parsexsd.h"

#include <cmath>
#include <cstdlib>
#include <cstring>
#include <set>
#include <string>
#include <utility>

#include "cpl_conv.h"
#include "cpl_error.h"
#include "cpl_http.h"
#include "cpl_minixml.h"
#include "cpl_string.h"
#include "ogr_core.h"

/************************************************************************/
/*                              StripNS()                               */
/*                                                                      */
/*      Return potentially shortened form of string with namespace      */
/*      stripped off if there is one.  Returns pointer into             */
/*      original string.                                                */
/************************************************************************/
static const char *StripNS(const char *pszFullValue)

{
    const char *pszColon = strstr(pszFullValue, ":");
    if (pszColon == nullptr)
        return pszFullValue;
    else
        return pszColon + 1;
}

/************************************************************************/
/*                   GetSimpleTypeProperties()                          */
/************************************************************************/

static bool GetSimpleTypeProperties(CPLXMLNode *psTypeNode,
                                    GMLPropertyType *pGMLType, int *pnWidth,
                                    int *pnPrecision)
{
    const char *pszBase =
        StripNS(CPLGetXMLValue(psTypeNode, "restriction.base", ""));

    if (EQUAL(pszBase, "decimal"))
    {
        *pGMLType = GMLPT_Real;
        const char *pszWidth =
            CPLGetXMLValue(psTypeNode, "restriction.totalDigits.value", "0");
        const char *pszPrecision =
            CPLGetXMLValue(psTypeNode, "restriction.fractionDigits.value", "0");
        *pnWidth = atoi(pszWidth);
        *pnPrecision = atoi(pszPrecision);
        return true;
    }

    else if (EQUAL(pszBase, "float"))
    {
        *pGMLType = GMLPT_Float;
        return true;
    }

    else if (EQUAL(pszBase, "double"))
    {
        *pGMLType = GMLPT_Real;
        return true;
    }

    else if (EQUAL(pszBase, "integer"))
    {
        *pGMLType = GMLPT_Integer;
        const char *pszWidth =
            CPLGetXMLValue(psTypeNode, "restriction.totalDigits.value", "0");
        *pnWidth = atoi(pszWidth);
        return true;
    }

    else if (EQUAL(pszBase, "long"))
    {
        *pGMLType = GMLPT_Integer64;
        const char *pszWidth =
            CPLGetXMLValue(psTypeNode, "restriction.totalDigits.value", "0");
        *pnWidth = atoi(pszWidth);
        return true;
    }

    else if (EQUAL(pszBase, "unsignedLong"))
    {
        // Optimistically map to signed integer...
        *pGMLType = GMLPT_Integer64;
        const char *pszWidth =
            CPLGetXMLValue(psTypeNode, "restriction.totalDigits.value", "0");
        *pnWidth = atoi(pszWidth);
        return true;
    }

    else if (EQUAL(pszBase, "string"))
    {
        *pGMLType = GMLPT_String;
        const char *pszWidth =
            CPLGetXMLValue(psTypeNode, "restriction.maxLength.value", "0");
        *pnWidth = atoi(pszWidth);
        return true;
    }

    else if (EQUAL(pszBase, "date"))
    {
        *pGMLType = GMLPT_Date;
        return true;
    }

    else if (EQUAL(pszBase, "time"))
    {
        *pGMLType = GMLPT_Time;
        return true;
    }

    else if (EQUAL(pszBase, "dateTime") || EQUAL(pszBase, "TimeInstantType"))
    {
        *pGMLType = GMLPT_DateTime;
        return true;
    }

    else if (EQUAL(pszBase, "boolean"))
    {
        *pGMLType = GMLPT_Boolean;
        return true;
    }

    else if (EQUAL(pszBase, "short"))
    {
        *pGMLType = GMLPT_Short;
        return true;
    }

    return false;
}

/************************************************************************/
/*                      LookForSimpleType()                             */
/************************************************************************/

static bool LookForSimpleType(CPLXMLNode *psSchemaNode,
                              const char *pszStrippedNSType,
                              GMLPropertyType *pGMLType, int *pnWidth,
                              int *pnPrecision)
{
    CPLXMLNode *psThis = psSchemaNode->psChild;
    for (; psThis != nullptr; psThis = psThis->psNext)
    {
        if (psThis->eType == CXT_Element &&
            EQUAL(psThis->pszValue, "simpleType") &&
            EQUAL(CPLGetXMLValue(psThis, "name", ""), pszStrippedNSType))
        {
            break;
        }
    }
    if (psThis == nullptr)
        return false;

    return GetSimpleTypeProperties(psThis, pGMLType, pnWidth, pnPrecision);
}

/************************************************************************/
/*                      GetSingleChildElement()                         */
/************************************************************************/

/* Returns the child element whose name is pszExpectedValue only if */
/* there is only one child that is an element. */
static CPLXMLNode *GetSingleChildElement(CPLXMLNode *psNode,
                                         const char *pszExpectedValue)
{
    if (psNode == nullptr)
        return nullptr;

    CPLXMLNode *psIter = psNode->psChild;
    if (psIter == nullptr)
        return nullptr;

    CPLXMLNode *psChild = nullptr;
    while (psIter != nullptr)
    {
        if (psIter->eType == CXT_Element)
        {
            if (psChild != nullptr)
                return nullptr;
            if (pszExpectedValue != nullptr &&
                strcmp(psIter->pszValue, pszExpectedValue) != 0)
                return nullptr;
            psChild = psIter;
        }
        psIter = psIter->psNext;
    }
    return psChild;
}

/************************************************************************/
/*                      CheckMinMaxOccursCardinality()                  */
/************************************************************************/

static int CheckMinMaxOccursCardinality(CPLXMLNode *psNode)
{
    const char *pszMinOccurs = CPLGetXMLValue(psNode, "minOccurs", nullptr);
    const char *pszMaxOccurs = CPLGetXMLValue(psNode, "maxOccurs", nullptr);
    return (pszMinOccurs == nullptr || EQUAL(pszMinOccurs, "0") ||
            EQUAL(pszMinOccurs, "1")) &&
           (pszMaxOccurs == nullptr || EQUAL(pszMaxOccurs, "1"));
}

/************************************************************************/
/*                     GetListTypeFromSingleType()                      */
/************************************************************************/

static GMLPropertyType GetListTypeFromSingleType(GMLPropertyType eType)
{
    if (eType == GMLPT_String)
        return GMLPT_StringList;
    if (eType == GMLPT_Integer || eType == GMLPT_Short)
        return GMLPT_IntegerList;
    if (eType == GMLPT_Integer64)
        return GMLPT_Integer64List;
    if (eType == GMLPT_Real || eType == GMLPT_Float)
        return GMLPT_RealList;
    if (eType == GMLPT_Boolean)
        return GMLPT_BooleanList;
    if (eType == GMLPT_FeatureProperty)
        return GMLPT_FeaturePropertyList;
    return eType;
}

/************************************************************************/
/*                      ParseFeatureType()                              */
/************************************************************************/

typedef struct
{
    const char *pszName;
    OGRwkbGeometryType eType;
} AssocNameType;

static const AssocNameType apsPropertyTypes[] = {
    {"GeometryPropertyType", wkbUnknown},
    {"PointPropertyType", wkbPoint},
    {"LineStringPropertyType", wkbLineString},
    {"CurvePropertyType", wkbCompoundCurve},
    {"PolygonPropertyType", wkbPolygon},
    {"SurfacePropertyType", wkbCurvePolygon},
    {"MultiPointPropertyType", wkbMultiPoint},
    {"MultiLineStringPropertyType", wkbMultiLineString},
    {"MultiCurvePropertyType", wkbMultiCurve},
    {"MultiPolygonPropertyType", wkbMultiPolygon},
    {"MultiSurfacePropertyType", wkbMultiSurface},
    {"MultiGeometryPropertyType", wkbGeometryCollection},
    {"GeometryAssociationType", wkbUnknown},
    {nullptr, wkbUnknown},
};

/* Found in FME .xsd  (e.g. <element ref="gml:curveProperty" minOccurs="0"/>) */
static const AssocNameType apsRefTypes[] = {
    {"pointProperty", wkbPoint},
    {"curveProperty", wkbLineString},  // Should we promote to wkbCompoundCurve?
    {"surfaceProperty", wkbPolygon},   // Should we promote to wkbCurvePolygon?
    {"multiPointProperty", wkbMultiPoint},
    {"multiCurveProperty", wkbMultiLineString},
    // Should we promote to wkbMultiSurface?
    {"multiSurfaceProperty", wkbMultiPolygon},
    {nullptr, wkbUnknown},
};

static GMLFeatureClass *GMLParseFeatureType(CPLXMLNode *psSchemaNode,
                                            const char *pszName,
                                            CPLXMLNode *psThis);

static GMLFeatureClass *GMLParseFeatureType(CPLXMLNode *psSchemaNode,
                                            const char *pszName,
                                            const char *pszType)
{
    CPLXMLNode *psThis = psSchemaNode->psChild;
    for (; psThis != nullptr; psThis = psThis->psNext)
    {
        if (psThis->eType == CXT_Element &&
            EQUAL(psThis->pszValue, "complexType") &&
            EQUAL(CPLGetXMLValue(psThis, "name", ""), pszType))
        {
            break;
        }
    }
    if (psThis == nullptr)
        return nullptr;

    return GMLParseFeatureType(psSchemaNode, pszName, psThis);
}

static GMLFeatureClass *GMLParseFeatureType(CPLXMLNode *psSchemaNode,
                                            const char *pszName,
                                            CPLXMLNode *psComplexType)
{

    /* -------------------------------------------------------------------- */
    /*      Grab the sequence of extensions greatgrandchild.                */
    /* -------------------------------------------------------------------- */
    CPLXMLNode *psAttrSeq =
        CPLGetXMLNode(psComplexType, "complexContent.extension.sequence");

    if (psAttrSeq == nullptr)
    {
        return nullptr;
    }

    /* -------------------------------------------------------------------- */
    /*      We are pretty sure this going to be a valid Feature class       */
    /*      now, so create it.                                              */
    /* -------------------------------------------------------------------- */
    GMLFeatureClass *poClass = new GMLFeatureClass(pszName);

    /* -------------------------------------------------------------------- */
    /*      Loop over each of the attribute elements being defined for      */
    /*      this feature class.                                             */
    /* -------------------------------------------------------------------- */
    int nAttributeIndex = 0;

    bool bGotUnrecognizedType = false;

    CPLXMLNode *psAttrDef = psAttrSeq->psChild;
    for (; psAttrDef != nullptr; psAttrDef = psAttrDef->psNext)
    {
        if (strcmp(psAttrDef->pszValue, "group") == 0)
        {
            /* Too complex schema for us. Aborts parsing */
            delete poClass;
            return nullptr;
        }

        /* Parse stuff like:
        <xs:choice>
            <xs:element ref="gml:polygonProperty"/>
            <xs:element ref="gml:multiPolygonProperty"/>
        </xs:choice>
        as found in
        https://downloadagiv.blob.core.windows.net/overstromingsgebieden-en-oeverzones/2014_01/Overstromingsgebieden_en_oeverzones_2014_01_GML.zip
        */
        if (strcmp(psAttrDef->pszValue, "choice") == 0)
        {
            CPLXMLNode *psChild = psAttrDef->psChild;
            bool bPolygon = false;
            bool bMultiPolygon = false;
            for (; psChild; psChild = psChild->psNext)
            {
                if (psChild->eType != CXT_Element)
                    continue;
                if (strcmp(psChild->pszValue, "element") == 0)
                {
                    const char *pszRef =
                        CPLGetXMLValue(psChild, "ref", nullptr);
                    if (pszRef != nullptr)
                    {
                        if (strcmp(pszRef, "gml:polygonProperty") == 0)
                        {
                            bPolygon = true;
                        }
                        else if (strcmp(pszRef, "gml:multiPolygonProperty") ==
                                 0)
                        {
                            bMultiPolygon = true;
                        }
                        else
                        {
                            delete poClass;
                            return nullptr;
                        }
                    }
                    else
                    {
                        delete poClass;
                        return nullptr;
                    }
                }
            }
            if (bPolygon && bMultiPolygon)
            {
                poClass->AddGeometryProperty(new GMLGeometryPropertyDefn(
                    "", "", wkbMultiPolygon, nAttributeIndex, true));

                nAttributeIndex++;
            }
            continue;
        }

        if (!EQUAL(psAttrDef->pszValue, "element"))
            continue;

        // MapServer WFS writes element type as an attribute of element
        // not as a simpleType definition.
        const char *pszType = CPLGetXMLValue(psAttrDef, "type", nullptr);
        const char *pszElementName = CPLGetXMLValue(psAttrDef, "name", nullptr);
        bool bNullable =
            EQUAL(CPLGetXMLValue(psAttrDef, "minOccurs", "1"), "0");
        const char *pszMaxOccurs =
            CPLGetXMLValue(psAttrDef, "maxOccurs", nullptr);
        if (pszType != nullptr)
        {
            const char *pszStrippedNSType = StripNS(pszType);
            int nWidth = 0;
            int nPrecision = 0;

            GMLPropertyType gmlType = GMLPT_Untyped;
            if (EQUAL(pszStrippedNSType, "string") ||
                EQUAL(pszStrippedNSType, "Character"))
                gmlType = GMLPT_String;
            else if (EQUAL(pszStrippedNSType, "date"))
                gmlType = GMLPT_Date;
            else if (EQUAL(pszStrippedNSType, "time"))
                gmlType = GMLPT_Time;
            else if (EQUAL(pszStrippedNSType, "dateTime") ||
                     EQUAL(pszStrippedNSType, "TimeInstantType"))
                gmlType = GMLPT_DateTime;
            else if (EQUAL(pszStrippedNSType, "real") ||
                     EQUAL(pszStrippedNSType, "double") ||
                     EQUAL(pszStrippedNSType, "decimal"))
                gmlType = GMLPT_Real;
            else if (EQUAL(pszStrippedNSType, "float"))
                gmlType = GMLPT_Float;
            else if (EQUAL(pszStrippedNSType, "int") ||
                     EQUAL(pszStrippedNSType, "integer"))
                gmlType = GMLPT_Integer;
            else if (EQUAL(pszStrippedNSType, "long"))
                gmlType = GMLPT_Integer64;
            else if (EQUAL(pszStrippedNSType, "unsignedLong"))
            {
                // Optimistically map to signed integer
                gmlType = GMLPT_Integer64;
            }
            else if (EQUAL(pszStrippedNSType, "short"))
                gmlType = GMLPT_Short;
            else if (EQUAL(pszStrippedNSType, "boolean"))
                gmlType = GMLPT_Boolean;
            // TODO: Would be nice to have a binary type.
            else if (EQUAL(pszStrippedNSType, "hexBinary"))
                gmlType = GMLPT_String;
            else if (strcmp(pszType, "gml:FeaturePropertyType") == 0)
            {
                gmlType = GMLPT_FeatureProperty;
            }
            else if (STARTS_WITH(pszType, "gml:"))
            {
                const AssocNameType *psIter = apsPropertyTypes;
                while (psIter->pszName)
                {
                    if (strncmp(pszType + 4, psIter->pszName,
                                strlen(psIter->pszName)) == 0)
                    {
                        OGRwkbGeometryType eType = psIter->eType;
                        std::string osSRSName;

                        // Look if there's a comment restricting to subclasses.
                        for (const CPLXMLNode *psIter2 = psAttrDef->psNext;
                             psIter2 != nullptr; psIter2 = psIter2->psNext)
                        {
                            if (psIter2->eType == CXT_Comment)
                            {
                                if (strstr(psIter2->pszValue,
                                           "restricted to Polygon"))
                                    eType = wkbPolygon;
                                else if (strstr(psIter2->pszValue,
                                                "restricted to LineString"))
                                    eType = wkbLineString;
                                else if (strstr(psIter2->pszValue,
                                                "restricted to MultiPolygon"))
                                    eType = wkbMultiPolygon;
                                else if (strstr(
                                             psIter2->pszValue,
                                             "restricted to MultiLineString"))
                                    eType = wkbMultiLineString;
                                else
                                {
                                    const char *pszSRSName =
                                        strstr(psIter2->pszValue, "srsName=\"");
                                    if (pszSRSName)
                                    {
                                        osSRSName =
                                            pszSRSName + strlen("srsName=\"");
                                        const auto nPos = osSRSName.find('"');
                                        if (nPos != std::string::npos)
                                            osSRSName.resize(nPos);
                                        else
                                            osSRSName.clear();
                                    }
                                }
                            }
                        }

                        // Try to get coordinate precision from a construct like:
                        /*
                            <xs:element name="wkb_geometry" type="gml:SurfacePropertyType" nillable="true" minOccurs="0" maxOccurs="1">
                                <xs:annotation>
                                  <xs:appinfo source="http://ogr.maptools.org/">
                                    <ogr:xy_coordinate_resolution>8.9e-9</ogr:xy_coordinate_resolution>
                                    <ogr:z_coordinate_resolution>1e-3</ogr:z_coordinate_resolution>
                                    <ogr:m_coordinate_resolution>1e-3</ogr:m_coordinate_resolution>
                                  </xs:appinfo>
                                </xs:annotation>
                            </xs:element>
                        */
                        OGRGeomCoordinatePrecision oGeomCoordPrec;
                        const auto psAnnotation =
                            CPLGetXMLNode(psAttrDef, "annotation");
                        if (psAnnotation)
                        {
                            for (const CPLXMLNode *psIterAppinfo =
                                     psAnnotation->psChild;
                                 psIterAppinfo;
                                 psIterAppinfo = psIterAppinfo->psNext)
                            {
                                if (psIterAppinfo->eType == CXT_Element &&
                                    strcmp(psIterAppinfo->pszValue,
                                           "appinfo") == 0 &&
                                    strcmp(CPLGetXMLValue(psIterAppinfo,
                                                          "source", ""),
                                           "http://ogr.maptools.org/") == 0)
                                {
                                    if (const char *pszXYRes = CPLGetXMLValue(
                                            psIterAppinfo,
                                            "xy_coordinate_resolution",
                                            nullptr))
                                    {
                                        const double dfVal = CPLAtof(pszXYRes);
                                        if (dfVal > 0 && std::isfinite(dfVal))
                                            oGeomCoordPrec.dfXYResolution =
                                                dfVal;
                                    }
                                    if (const char *pszZRes = CPLGetXMLValue(
                                            psIterAppinfo,
                                            "z_coordinate_resolution", nullptr))
                                    {
                                        const double dfVal = CPLAtof(pszZRes);
                                        if (dfVal > 0 && std::isfinite(dfVal))
                                            oGeomCoordPrec.dfZResolution =
                                                dfVal;
                                    }
                                }
                            }
                        }

                        GMLGeometryPropertyDefn *poDefn =
                            new GMLGeometryPropertyDefn(
                                pszElementName, pszElementName, eType,
                                nAttributeIndex, bNullable, oGeomCoordPrec);
                        poDefn->SetSRSName(osSRSName);

                        if (poClass->AddGeometryProperty(poDefn) < 0)
                            delete poDefn;
                        else
                            nAttributeIndex++;

                        break;
                    }

                    psIter++;
                }

                if (psIter->pszName == nullptr)
                {
                    // Can be a non geometry gml type.
                    // Too complex schema for us. Aborts parsing.
                    delete poClass;
                    return nullptr;
                }

                if (poClass->GetGeometryPropertyCount() == 0)
                    bGotUnrecognizedType = true;

                continue;
            }

            /* Integraph stuff */
            else if (strcmp(pszType, "G:Point_MultiPointPropertyType") == 0 ||
                     strcmp(pszType, "gmgml:Point_MultiPointPropertyType") == 0)
            {
                GMLGeometryPropertyDefn *poDefn = new GMLGeometryPropertyDefn(
                    pszElementName, pszElementName, wkbMultiPoint,
                    nAttributeIndex, bNullable);

                if (poClass->AddGeometryProperty(poDefn) < 0)
                    delete poDefn;
                else
                    nAttributeIndex++;

                continue;
            }
            else if (strcmp(pszType,
                            "G:LineString_MultiLineStringPropertyType") == 0 ||
                     strcmp(pszType,
                            "gmgml:LineString_MultiLineStringPropertyType") ==
                         0)
            {
                GMLGeometryPropertyDefn *poDefn = new GMLGeometryPropertyDefn(
                    pszElementName, pszElementName, wkbMultiLineString,
                    nAttributeIndex, bNullable);

                if (poClass->AddGeometryProperty(poDefn) < 0)
                    delete poDefn;
                else
                    nAttributeIndex++;

                continue;
            }
            else if (strcmp(pszType, "G:Polygon_MultiPolygonPropertyType") ==
                         0 ||
                     strcmp(pszType,
                            "gmgml:Polygon_MultiPolygonPropertyType") == 0 ||
                     strcmp(pszType, "gmgml:Polygon_Surface_MultiSurface_"
                                     "CompositeSurfacePropertyType") == 0)
            {
                GMLGeometryPropertyDefn *poDefn = new GMLGeometryPropertyDefn(
                    pszElementName, pszElementName, wkbMultiPolygon,
                    nAttributeIndex, bNullable);

                if (poClass->AddGeometryProperty(poDefn) < 0)
                    delete poDefn;
                else
                    nAttributeIndex++;

                continue;
            }

            // ERDAS Apollo stufflike in
            // http://apollo.erdas.com/erdas-apollo/vector/WORLDWIDE?SERVICE=WFS&VERSION=1.0.0&REQUEST=DescribeFeatureType&TYPENAME=wfs:cntry98)
            else if (strcmp(pszType, "wfs:MixedPolygonPropertyType") == 0)
            {
                GMLGeometryPropertyDefn *poDefn = new GMLGeometryPropertyDefn(
                    pszElementName, pszElementName, wkbMultiPolygon,
                    nAttributeIndex, bNullable);

                if (poClass->AddGeometryProperty(poDefn) < 0)
                    delete poDefn;
                else
                    nAttributeIndex++;

                continue;
            }

            else
            {
                gmlType = GMLPT_Untyped;
                if (!LookForSimpleType(psSchemaNode, pszStrippedNSType,
                                       &gmlType, &nWidth, &nPrecision))
                {
                    // Too complex schema for us. Aborts parsing.
                    delete poClass;
                    return nullptr;
                }
            }

            if (pszElementName == nullptr)
                pszElementName = "unnamed";
            const char *pszPropertyName = pszElementName;
            if (gmlType == GMLPT_FeatureProperty)
            {
                pszPropertyName = CPLSPrintf("%s_href", pszElementName);
            }
            GMLPropertyDefn *poProp =
                new GMLPropertyDefn(pszPropertyName, pszElementName);

            if (pszMaxOccurs != nullptr && strcmp(pszMaxOccurs, "1") != 0)
                gmlType = GetListTypeFromSingleType(gmlType);

            poProp->SetType(gmlType);
            poProp->SetWidth(nWidth);
            poProp->SetPrecision(nPrecision);
            poProp->SetNullable(bNullable);

            const CPLXMLNode *psAnnotation =
                CPLGetXMLNode(psAttrDef, "annotation");
            if (psAnnotation)
            {
                const char *pszDocumentation =
                    CPLGetXMLValue(psAnnotation, "documentation", nullptr);
                if (pszDocumentation)
                    poProp->SetDocumentation(pszDocumentation);
            }

            if (poClass->AddProperty(poProp) < 0)
                delete poProp;
            else
                nAttributeIndex++;

            continue;
        }

        // For now we skip geometries.  Fixup later.
        CPLXMLNode *psSimpleType = CPLGetXMLNode(psAttrDef, "simpleType");
        if (psSimpleType == nullptr)
        {
            const char *pszRef = CPLGetXMLValue(psAttrDef, "ref", nullptr);

            // FME .xsd
            if (pszRef != nullptr && STARTS_WITH(pszRef, "gml:"))
            {
                const AssocNameType *psIter = apsRefTypes;
                while (psIter->pszName)
                {
                    if (strncmp(pszRef + 4, psIter->pszName,
                                strlen(psIter->pszName)) == 0)
                    {
                        if (poClass->GetGeometryPropertyCount() > 0)
                        {
                            OGRwkbGeometryType eNewType = psIter->eType;
                            OGRwkbGeometryType eOldType =
                                static_cast<OGRwkbGeometryType>(
                                    poClass->GetGeometryProperty(0)->GetType());

                            if ((eNewType == wkbMultiPoint &&
                                 eOldType == wkbPoint) ||
                                (eNewType == wkbMultiLineString &&
                                 eOldType == wkbLineString) ||
                                (eNewType == wkbMultiPolygon &&
                                 eOldType == wkbPolygon))
                            {
                                poClass->GetGeometryProperty(0)->SetType(
                                    eNewType);
                            }
                            else
                            {
                                CPLDebug("GML",
                                         "Geometry field already found ! "
                                         "Ignoring the following ones");
                            }
                        }
                        else
                        {
                            GMLGeometryPropertyDefn *poDefn =
                                new GMLGeometryPropertyDefn(
                                    pszElementName, pszElementName,
                                    psIter->eType, nAttributeIndex, true);

                            if (poClass->AddGeometryProperty(poDefn) < 0)
                                delete poDefn;
                            else
                                nAttributeIndex++;
                        }

                        break;
                    }

                    psIter++;
                }

                if (psIter->pszName == nullptr)
                {
                    // Can be a non geometry gml type .
                    // Too complex schema for us. Aborts parsing.
                    delete poClass;
                    return nullptr;
                }

                if (poClass->GetGeometryPropertyCount() == 0)
                    bGotUnrecognizedType = true;

                continue;
            }

            /* Parse stuff like the following found in
            http://199.29.1.81:8181/miwfs/GetFeature.ashx?REQUEST=GetFeature&MAXFEATURES=1&SERVICE=WFS&VERSION=1.0.0&TYPENAME=miwfs:World
            : <xs:element name="Obj" minOccurs="0" maxOccurs="1">
                <xs:complexType>
                    <xs:sequence>
                        <xs:element ref="gml:_Geometry"/>
                    </xs:sequence>
                </xs:complexType>
            </xs:element>
            */
            CPLXMLNode *l_psComplexType =
                GetSingleChildElement(psAttrDef, "complexType");
            CPLXMLNode *psComplexTypeSequence =
                GetSingleChildElement(l_psComplexType, "sequence");
            CPLXMLNode *psComplexTypeSequenceElement =
                GetSingleChildElement(psComplexTypeSequence, "element");

            if (pszElementName != nullptr &&
                CheckMinMaxOccursCardinality(psAttrDef) &&
                psComplexTypeSequenceElement != nullptr &&
                CheckMinMaxOccursCardinality(psComplexTypeSequence) &&
                strcmp(CPLGetXMLValue(psComplexTypeSequenceElement, "ref", ""),
                       "gml:_Geometry") == 0)
            {
                GMLGeometryPropertyDefn *poDefn = new GMLGeometryPropertyDefn(
                    pszElementName, pszElementName, wkbUnknown, nAttributeIndex,
                    bNullable);

                if (poClass->AddGeometryProperty(poDefn) < 0)
                    delete poDefn;
                else
                    nAttributeIndex++;

                continue;
            }
            else
            {
                // Too complex schema for us. Aborts parsing.
                delete poClass;
                return nullptr;
            }
        }

        if (pszElementName == nullptr)
            pszElementName = "unnamed";
        GMLPropertyDefn *poProp =
            new GMLPropertyDefn(pszElementName, pszElementName);

        GMLPropertyType eType = GMLPT_Untyped;
        int nWidth = 0;
        int nPrecision = 0;
        GetSimpleTypeProperties(psSimpleType, &eType, &nWidth, &nPrecision);

        if (pszMaxOccurs != nullptr && strcmp(pszMaxOccurs, "1") != 0)
            eType = GetListTypeFromSingleType(eType);

        poProp->SetType(eType);
        poProp->SetWidth(nWidth);
        poProp->SetPrecision(nPrecision);
        poProp->SetNullable(bNullable);

        const CPLXMLNode *psAnnotation = CPLGetXMLNode(psAttrDef, "annotation");
        if (psAnnotation)
        {
            const char *pszDocumentation =
                CPLGetXMLValue(psAnnotation, "documentation", nullptr);
            if (pszDocumentation)
                poProp->SetDocumentation(pszDocumentation);
        }

        if (poClass->AddProperty(poProp) < 0)
            delete poProp;
        else
            nAttributeIndex++;
    }

    // If we have found an unknown types, let's be on the side of caution and
    // create a geometry field.
    if (poClass->GetGeometryPropertyCount() == 0 && bGotUnrecognizedType)
    {
        poClass->AddGeometryProperty(
            new GMLGeometryPropertyDefn("", "", wkbUnknown, -1, true));
    }

    /* -------------------------------------------------------------------- */
    /*      Class complete, add to reader class list.                       */
    /* -------------------------------------------------------------------- */
    poClass->SetSchemaLocked(true);

    return poClass;
}

/************************************************************************/
/*                         ExcludeBaseGMLSchemas()                      */
/************************************************************************/

static bool ExcludeBaseGMLSchemas(const std::string &osFilename)
{
    // List of substrings to exclude
    const std::vector<std::string> excludedBaseGMLReferencedSchemasList = {
        "/gml/3.2.1/", "gml/3.1.1/", "/gml/2.1.2/", "/gmlsfProfile/"};
    for (const auto &pattern : excludedBaseGMLReferencedSchemasList)
    {
        if (osFilename.find(pattern) != std::string::npos)
        {
            return false;  // Found one of the excluded base GML referenced schema
        }
    }
    return true;  // None of the base GML referenced schemas were found
}

/************************************************************************/
/*                         GMLParseXMLFile()                            */
/************************************************************************/

static CPLXMLNode *GMLParseXMLFile(const char *pszFilename)
{
    if (STARTS_WITH(pszFilename, "http://") ||
        STARTS_WITH(pszFilename, "https://"))
    {
        CPLXMLNode *psRet = nullptr;
        CPLHTTPResult *psResult = CPLHTTPFetch(pszFilename, nullptr);
        if (psResult != nullptr)
        {
            if (psResult->pabyData != nullptr)
            {
                psRet = CPLParseXMLString(
                    reinterpret_cast<const char *>(psResult->pabyData));
            }
            CPLHTTPDestroyResult(psResult);
        }
        return psRet;
    }
    else
    {
        return CPLParseXMLFile(pszFilename);
    }
}

/************************************************************************/
/*                       CPLGetFirstChildNode()                         */
/************************************************************************/

static CPLXMLNode *CPLGetFirstChildNode(CPLXMLNode *psNode)
{
    if (psNode == nullptr)
        return nullptr;
    CPLXMLNode *psIter = psNode->psChild;
    while (psIter != nullptr)
    {
        if (psIter->eType == CXT_Element)
            return psIter;
        psIter = psIter->psNext;
    }
    return nullptr;
}

/************************************************************************/
/*                          CPLGetLastNode()                            */
/************************************************************************/

static CPLXMLNode *CPLGetLastNode(CPLXMLNode *psNode)
{
    CPLXMLNode *psIter = psNode;
    while (psIter->psNext != nullptr)
        psIter = psIter->psNext;
    return psIter;
}

/************************************************************************/
/*                       CPLXMLSchemaResolveInclude()                   */
/************************************************************************/

static void CPLXMLSchemaResolveInclude(const char *pszMainSchemaLocation,
                                       CPLXMLNode *psSchemaNode,
                                       bool bUseSchemaImports)
{
    std::set<CPLString> osAlreadyIncluded;

    bool bTryAgain;
    do
    {
        CPLXMLNode *psLast = nullptr;
        bTryAgain = false;

        CPLXMLNode *psThis = psSchemaNode->psChild;
        for (; psThis != nullptr; psThis = psThis->psNext)
        {
            std::string osSchemaLocation =
                CPLGetXMLValue(psThis, "schemaLocation", "");

            if (psThis->eType == CXT_Element &&
                (EQUAL(psThis->pszValue, "include") ||
                 (bUseSchemaImports == TRUE &&
                  EQUAL(psThis->pszValue, "import") &&
                  ExcludeBaseGMLSchemas(osSchemaLocation))))
            {

                if (!osSchemaLocation.empty() &&
                    osAlreadyIncluded.count(osSchemaLocation) == 0)
                {
                    osAlreadyIncluded.insert(osSchemaLocation);

                    if (!STARTS_WITH(osSchemaLocation.c_str(), "http://") &&
                        !STARTS_WITH(osSchemaLocation.c_str(), "https://") &&
                        CPLIsFilenameRelative(osSchemaLocation.c_str()))
                    {
                        osSchemaLocation = CPLFormFilenameSafe(
                            CPLGetPathSafe(pszMainSchemaLocation).c_str(),
                            osSchemaLocation.c_str(), nullptr);
                    }

                    CPLXMLNode *psIncludedXSDTree =
                        GMLParseXMLFile(osSchemaLocation.c_str());
                    if (psIncludedXSDTree != nullptr)
                    {
                        CPLStripXMLNamespace(psIncludedXSDTree, nullptr, TRUE);
                        CPLXMLNode *psIncludedSchemaNode =
                            CPLGetXMLNode(psIncludedXSDTree, "=schema");
                        if (psIncludedSchemaNode != nullptr)
                        {
                            // Substitute de <include> node by its content.
                            CPLXMLNode *psFirstChildElement =
                                CPLGetFirstChildNode(psIncludedSchemaNode);
                            if (psFirstChildElement != nullptr)
                            {
                                CPLXMLNode *psCopy =
                                    CPLCloneXMLTree(psFirstChildElement);
                                if (psLast != nullptr)
                                    psLast->psNext = psCopy;
                                else
                                    psSchemaNode->psChild = psCopy;
                                CPLXMLNode *psNext = psThis->psNext;
                                psThis->psNext = nullptr;
                                CPLDestroyXMLNode(psThis);
                                psThis = CPLGetLastNode(psCopy);
                                psThis->psNext = psNext;

                                // In case the included schema also contains
                                // includes.
                                bTryAgain = true;
                            }
                        }
                        CPLDestroyXMLNode(psIncludedXSDTree);
                    }
                }
            }

            psLast = psThis;
        }
    } while (bTryAgain);

    const char *pszSchemaOutputName =
        CPLGetConfigOption("GML_SCHEMA_OUTPUT_NAME", nullptr);
    if (pszSchemaOutputName != nullptr)
    {
        CPLSerializeXMLTreeToFile(psSchemaNode, pszSchemaOutputName);
    }
}

/************************************************************************/
/*                       GetUniqueConstraints()                         */
/************************************************************************/

static std::set<std::pair<std::string, std::string>>
GetUniqueConstraints(const CPLXMLNode *psNode)
{
    /* Parse
        <xs:unique name="uniqueConstraintpolyeas_id">
            <xs:selector xpath="ogr:featureMember/ogr:poly"/>
            <xs:field xpath="ogr:eas_id"/>
        </xs:unique>
    */
    std::set<std::pair<std::string, std::string>> oSet;
    for (const auto *psIter = psNode->psChild; psIter != nullptr;
         psIter = psIter->psNext)
    {
        if (psIter->eType == CXT_Element && EQUAL(psIter->pszValue, "unique"))
        {
            const char *pszSelector =
                CPLGetXMLValue(psIter, "selector.xpath", nullptr);
            const char *pszField =
                CPLGetXMLValue(psIter, "field.xpath", nullptr);
            if (pszSelector && pszField && pszField[0] != '@')
            {
                const char *pszSlash = strchr(pszSelector, '/');
                if (pszSlash)
                {
                    oSet.insert(
                        std::pair(StripNS(pszSlash + 1), StripNS(pszField)));
                }
            }
        }
    }
    return oSet;
}

/************************************************************************/
/*                          GMLParseXSD()                               */
/************************************************************************/

bool GMLParseXSD(const char *pszFile, bool bUseSchemaImports,
                 std::vector<GMLFeatureClass *> &aosClasses,
                 bool &bFullyUnderstood)

{
    bFullyUnderstood = false;

    if (pszFile == nullptr)
        return false;

    /* -------------------------------------------------------------------- */
    /*      Load the raw XML file.                                          */
    /* -------------------------------------------------------------------- */
    CPLXMLNode *psXSDTree = GMLParseXMLFile(pszFile);

    if (psXSDTree == nullptr)
        return false;

    /* -------------------------------------------------------------------- */
    /*      Strip off any namespace qualifiers.                             */
    /* -------------------------------------------------------------------- */
    CPLStripXMLNamespace(psXSDTree, nullptr, TRUE);

    /* -------------------------------------------------------------------- */
    /*      Find <schema> root element.                                     */
    /* -------------------------------------------------------------------- */
    CPLXMLNode *psSchemaNode = CPLGetXMLNode(psXSDTree, "=schema");
    if (psSchemaNode == nullptr)
    {
        CPLDestroyXMLNode(psXSDTree);
        return false;
    }

    /* ==================================================================== */
    /*      Process each include directive.                                 */
    /* ==================================================================== */
    CPLXMLSchemaResolveInclude(pszFile, psSchemaNode, bUseSchemaImports);

    // CPLSerializeXMLTreeToFile(psSchemaNode, "/vsistdout/");

    bFullyUnderstood = true;

    /* ==================================================================== */
    /*      Process each feature class definition.                          */
    /* ==================================================================== */
    CPLXMLNode *psThis = psSchemaNode->psChild;

    std::set<std::pair<std::string, std::string>> oSetUniqueConstraints;

    for (; psThis != nullptr; psThis = psThis->psNext)
    {
        /* --------------------------------------------------------------------
         */
        /*      Check for <xs:element> node. */
        /* --------------------------------------------------------------------
         */
        if (psThis->eType != CXT_Element || !EQUAL(psThis->pszValue, "element"))
            continue;

        /* --------------------------------------------------------------------
         */
        /*      Get name */
        /* --------------------------------------------------------------------
         */
        const char *pszName = CPLGetXMLValue(psThis, "name", nullptr);
        if (pszName == nullptr)
        {
            continue;
        }

        /* --------------------------------------------------------------------
         */
        /*      Check the substitution group. */
        /* --------------------------------------------------------------------
         */
        const char *pszSubGroup =
            StripNS(CPLGetXMLValue(psThis, "substitutionGroup", ""));

        if (EQUAL(pszName, "FeatureCollection") &&
            (EQUAL(pszSubGroup, "_FeatureCollection") ||
             EQUAL(pszSubGroup, "_GML") ||
             EQUAL(pszSubGroup, "AbstractFeature")))
        {
            oSetUniqueConstraints = GetUniqueConstraints(psThis);
            continue;
        }

        // AbstractFeature used by GML 3.2.
        if (!EQUAL(pszSubGroup, "_Feature") &&
            !EQUAL(pszSubGroup, "AbstractFeature"))
        {
            continue;
        }

        /* --------------------------------------------------------------------
         */
        /*      Get type and verify relationship with name. */
        /* --------------------------------------------------------------------
         */
        const char *pszType = CPLGetXMLValue(psThis, "type", nullptr);
        if (pszType == nullptr)
        {
            CPLXMLNode *psComplexType = CPLGetXMLNode(psThis, "complexType");
            if (psComplexType)
            {
                GMLFeatureClass *poClass =
                    GMLParseFeatureType(psSchemaNode, pszName, psComplexType);
                if (poClass)
                    aosClasses.push_back(poClass);
                else
                    bFullyUnderstood = false;
            }
            continue;
        }
        if (strstr(pszType, ":") != nullptr)
            pszType = strstr(pszType, ":") + 1;
        if (EQUAL(pszType, pszName))
        {
            // A few WFS servers return a type name which is the element name
            // without any _Type or Type suffix
            // e.g.:
            // http://apollo.erdas.com/erdas-apollo/vector/Cherokee?SERVICE=WFS&VERSION=1.0.0&REQUEST=DescribeFeatureType&TYPENAME=iwfs:Air
            // */

            // TODO(schwehr): What was supposed to go here?
        }

        // <element name="RekisteriyksikonPalstanTietoja"
        // type="ktjkiiwfs:PalstanTietojaType" substitutionGroup="gml:_Feature"
        // />
        else if (strlen(pszType) > 4 &&
                 strcmp(pszType + strlen(pszType) - 4, "Type") == 0 &&
                 strlen(pszName) > strlen(pszType) - 4 &&
                 strncmp(pszName + strlen(pszName) - (strlen(pszType) - 4),
                         pszType, strlen(pszType) - 4) == 0)
        {
        }

        else if (!EQUALN(pszType, pszName, strlen(pszName)) ||
                 !(EQUAL(pszType + strlen(pszName), "_Type") ||
                   EQUAL(pszType + strlen(pszName), "Type") ||
                   EQUAL(pszType + strlen(pszName), "FeatureType")))
        {
            continue;
        }

        // CanVec .xsd contains weird types that are not used in the related
        // GML.
        if (STARTS_WITH(pszName, "XyZz") || STARTS_WITH(pszName, "XyZ1") ||
            STARTS_WITH(pszName, "XyZ2"))
            continue;

        GMLFeatureClass *poClass =
            GMLParseFeatureType(psSchemaNode, pszName, pszType);
        if (poClass)
            aosClasses.push_back(poClass);
        else
            bFullyUnderstood = false;
    }

    CPLDestroyXMLNode(psXSDTree);

    // Attach unique constraints to fields
    for (const auto &typeFieldPair : oSetUniqueConstraints)
    {
        for (const auto *poClass : aosClasses)
        {
            if (poClass->GetName() == typeFieldPair.first)
            {
                auto poProperty =
                    poClass->GetProperty(typeFieldPair.second.c_str());
                if (poProperty)
                {
                    poProperty->SetUnique(true);
                }
                break;
            }
        }
    }

    return !aosClasses.empty();
}
