/******************************************************************************
 *
 * Project:  OpenGIS Simple Features Reference Implementation
 * Purpose:  Implementation of GeoJSON writer utilities (OGR GeoJSON Driver).
 * Author:   Mateusz Loskot, mateusz@loskot.net
 *
 ******************************************************************************
 * Copyright (c) 2007, Mateusz Loskot
 * Copyright (c) 2008-2014, Even Rouault <even dot rouault at spatialys.com>
 *
 * SPDX-License-Identifier: MIT
 ****************************************************************************/

/*! @cond Doxygen_Suppress */

#define JSON_C_VER_013 (13 << 8)

#include "ogrgeojsonwriter.h"
#include "ogr_geometry.h"
#include "ogrgeojsongeometry.h"
#include "ogrlibjsonutils.h"
#include "ogr_feature.h"
#include "ogr_p.h"
#include <json.h>  // JSON-C

#if (!defined(JSON_C_VERSION_NUM)) || (JSON_C_VERSION_NUM < JSON_C_VER_013)
#include <json_object_private.h>
#endif

#include <printbuf.h>
#include "ogr_api.h"

#include <algorithm>
#include <cmath>
#include <cstdint>
#include <limits>
#include <optional>

static json_object *
json_object_new_float_with_significant_figures(float fVal,
                                               int nSignificantFigures);

static json_object *
OGRGeoJSONWritePoint(const OGRPoint *poPoint,
                     const OGRGeoJSONWriteOptions &oOptions);

static json_object *
OGRGeoJSONWriteSimpleCurve(const OGRSimpleCurve *poLine,
                           const OGRGeoJSONWriteOptions &oOptions);

static json_object *
OGRGeoJSONWriteMultiPoint(const OGRMultiPoint *poGeometry,
                          const OGRGeoJSONWriteOptions &oOptions);

static json_object *
OGRGeoJSONWriteMultiLineString(const OGRMultiLineString *poGeometry,
                               const OGRGeoJSONWriteOptions &oOptions);

static json_object *
OGRGeoJSONWriteMultiPolygon(const OGRMultiPolygon *poGeometry,
                            const OGRGeoJSONWriteOptions &oOptions);

static json_object *
OGRGeoJSONWriteGeometryCollection(const OGRGeometryCollection *poGeometry,
                                  const OGRGeoJSONWriteOptions &oOptions);

static json_object *
OGRGeoJSONWriteCoords(double dfX, double dfY, std::optional<double> dfZ,
                      std::optional<double> dfM,
                      const OGRGeoJSONWriteOptions &oOptions);

static json_object *
OGRGeoJSONWriteLineCoords(const OGRSimpleCurve *poLine,
                          const OGRGeoJSONWriteOptions &oOptions);

static json_object *
OGRGeoJSONWriteRingCoords(const OGRLinearRing *poLine, bool bIsExteriorRing,
                          const OGRGeoJSONWriteOptions &oOptions);

static json_object *
OGRGeoJSONWriteCompoundCurve(const OGRCompoundCurve *poCC,
                             const OGRGeoJSONWriteOptions &oOptions);

static json_object *
OGRGeoJSONWriteCurvePolygon(const OGRCurvePolygon *poCP,
                            const OGRGeoJSONWriteOptions &oOptions);

/************************************************************************/
/*                         SetRFC7946Settings()                         */
/************************************************************************/

/*! @cond Doxygen_Suppress */
void OGRGeoJSONWriteOptions::SetRFC7946Settings()
{
    bBBOXRFC7946 = true;
    if (nXYCoordPrecision < 0)
        nXYCoordPrecision = 7;
    if (nZCoordPrecision < 0)
        nZCoordPrecision = 3;
    bPolygonRightHandRule = true;
    bCanPatchCoordinatesWithNativeData = false;
    bHonourReservedRFC7946Members = true;
}

void OGRGeoJSONWriteOptions::SetIDOptions(CSLConstList papszOptions)
{

    osIDField = CSLFetchNameValueDef(papszOptions, "ID_FIELD", "");
    const char *pszIDFieldType = CSLFetchNameValue(papszOptions, "ID_TYPE");
    if (pszIDFieldType)
    {
        if (EQUAL(pszIDFieldType, "String"))
        {
            bForceIDFieldType = true;
            eForcedIDFieldType = OFTString;
        }
        else if (EQUAL(pszIDFieldType, "Integer"))
        {
            bForceIDFieldType = true;
            eForcedIDFieldType = OFTInteger64;
        }
    }
    bGenerateID =
        CPL_TO_BOOL(CSLFetchBoolean(papszOptions, "ID_GENERATE", false));
}

/*! @endcond */

/************************************************************************/
/*                        json_object_new_coord()                       */
/************************************************************************/

static json_object *
json_object_new_coord(double dfVal, int nDimIdx,
                      const OGRGeoJSONWriteOptions &oOptions)
{
    // If coordinate precision is specified, or significant figures is not
    // then use the '%f' formatting.
    if (nDimIdx <= 2)
    {
        if (oOptions.nXYCoordPrecision >= 0 || oOptions.nSignificantFigures < 0)
            return json_object_new_double_with_precision(
                dfVal, oOptions.nXYCoordPrecision);
    }
    else
    {
        if (oOptions.nZCoordPrecision >= 0 || oOptions.nSignificantFigures < 0)
            return json_object_new_double_with_precision(
                dfVal, oOptions.nZCoordPrecision);
    }

    return json_object_new_double_with_significant_figures(
        dfVal, oOptions.nSignificantFigures);
}

/************************************************************************/
/*                     OGRGeoJSONIsPatchablePosition()                  */
/************************************************************************/

static bool OGRGeoJSONIsPatchablePosition(json_object *poJSonCoordinates,
                                          json_object *poNativeCoordinates)
{
    return json_object_get_type(poJSonCoordinates) == json_type_array &&
           json_object_get_type(poNativeCoordinates) == json_type_array &&
           json_object_array_length(poJSonCoordinates) == 3 &&
           json_object_array_length(poNativeCoordinates) >= 4 &&
           json_object_get_type(json_object_array_get_idx(
               poJSonCoordinates, 0)) != json_type_array &&
           json_object_get_type(json_object_array_get_idx(
               poNativeCoordinates, 0)) != json_type_array;
}

/************************************************************************/
/*                    OGRGeoJSONIsCompatiblePosition()                  */
/************************************************************************/

static bool OGRGeoJSONIsCompatiblePosition(json_object *poJSonCoordinates,
                                           json_object *poNativeCoordinates)
{
    return json_object_get_type(poJSonCoordinates) == json_type_array &&
           json_object_get_type(poNativeCoordinates) == json_type_array &&
           json_object_array_length(poJSonCoordinates) ==
               json_object_array_length(poNativeCoordinates) &&
           json_object_get_type(json_object_array_get_idx(
               poJSonCoordinates, 0)) != json_type_array &&
           json_object_get_type(json_object_array_get_idx(
               poNativeCoordinates, 0)) != json_type_array;
}

/************************************************************************/
/*                       OGRGeoJSONPatchPosition()                      */
/************************************************************************/

static void OGRGeoJSONPatchPosition(json_object *poJSonCoordinates,
                                    json_object *poNativeCoordinates)
{
    const auto nLength = json_object_array_length(poNativeCoordinates);
    for (auto i = decltype(nLength){3}; i < nLength; i++)
    {
        json_object_array_add(
            poJSonCoordinates,
            json_object_get(json_object_array_get_idx(poNativeCoordinates, i)));
    }
}

/************************************************************************/
/*                      OGRGeoJSONIsPatchableArray()                    */
/************************************************************************/

static bool OGRGeoJSONIsPatchableArray(json_object *poJSonArray,
                                       json_object *poNativeArray, int nDepth)
{
    if (nDepth == 0)
        return OGRGeoJSONIsPatchablePosition(poJSonArray, poNativeArray);

    if (json_object_get_type(poJSonArray) == json_type_array &&
        json_object_get_type(poNativeArray) == json_type_array)
    {
        const auto nLength = json_object_array_length(poJSonArray);
        if (nLength == json_object_array_length(poNativeArray))
        {
            if (nLength > 0)
            {
                json_object *poJSonChild =
                    json_object_array_get_idx(poJSonArray, 0);
                json_object *poNativeChild =
                    json_object_array_get_idx(poNativeArray, 0);
                if (!OGRGeoJSONIsPatchableArray(poJSonChild, poNativeChild,
                                                nDepth - 1))
                {
                    return false;
                }
                // Light check as a former extensive check was done in
                // OGRGeoJSONComputePatchableOrCompatibleArray
            }
            return true;
        }
    }
    return false;
}

/************************************************************************/
/*                OGRGeoJSONComputePatchableOrCompatibleArray()         */
/************************************************************************/

/* Returns true if the objects are comparable, ie Point vs Point, LineString
   vs LineString, but they might not be patchable or compatible */
static bool OGRGeoJSONComputePatchableOrCompatibleArrayInternal(
    json_object *poJSonArray, json_object *poNativeArray, int nDepth,
    bool &bOutPatchable, bool &bOutCompatible)
{
    if (nDepth == 0)
    {
        bOutPatchable &=
            OGRGeoJSONIsPatchablePosition(poJSonArray, poNativeArray);
        bOutCompatible &=
            OGRGeoJSONIsCompatiblePosition(poJSonArray, poNativeArray);
        return json_object_get_type(poJSonArray) == json_type_array &&
               json_object_get_type(poNativeArray) == json_type_array &&
               json_object_get_type(json_object_array_get_idx(
                   poJSonArray, 0)) != json_type_array &&
               json_object_get_type(json_object_array_get_idx(
                   poNativeArray, 0)) != json_type_array;
    }

    if (json_object_get_type(poJSonArray) == json_type_array &&
        json_object_get_type(poNativeArray) == json_type_array)
    {
        const auto nLength = json_object_array_length(poJSonArray);
        if (nLength == json_object_array_length(poNativeArray))
        {
            for (auto i = decltype(nLength){0}; i < nLength; i++)
            {
                json_object *poJSonChild =
                    json_object_array_get_idx(poJSonArray, i);
                json_object *poNativeChild =
                    json_object_array_get_idx(poNativeArray, i);
                if (!OGRGeoJSONComputePatchableOrCompatibleArrayInternal(
                        poJSonChild, poNativeChild, nDepth - 1, bOutPatchable,
                        bOutCompatible))
                {
                    return false;
                }
                if (!bOutPatchable && !bOutCompatible)
                    break;
            }
            return true;
        }
    }

    bOutPatchable = false;
    bOutCompatible = false;
    return false;
}

/* Returns true if the objects are comparable, ie Point vs Point, LineString
   vs LineString, but they might not be patchable or compatible */
static bool OGRGeoJSONComputePatchableOrCompatibleArray(
    json_object *poJSonArray, json_object *poNativeArray, int nDepth,
    bool &bOutPatchable, bool &bOutCompatible)
{
    bOutPatchable = true;
    bOutCompatible = true;
    return OGRGeoJSONComputePatchableOrCompatibleArrayInternal(
        poJSonArray, poNativeArray, nDepth, bOutPatchable, bOutCompatible);
}

/************************************************************************/
/*                        OGRGeoJSONPatchArray()                        */
/************************************************************************/

static void OGRGeoJSONPatchArray(json_object *poJSonArray,
                                 json_object *poNativeArray, int nDepth)
{
    if (nDepth == 0)
    {
        OGRGeoJSONPatchPosition(poJSonArray, poNativeArray);
        return;
    }
    const auto nLength = json_object_array_length(poJSonArray);
    for (auto i = decltype(nLength){0}; i < nLength; i++)
    {
        json_object *poJSonChild = json_object_array_get_idx(poJSonArray, i);
        json_object *poNativeChild =
            json_object_array_get_idx(poNativeArray, i);
        OGRGeoJSONPatchArray(poJSonChild, poNativeChild, nDepth - 1);
    }
}

/************************************************************************/
/*                        OGRGeoJSONIsPatchableGeometry()                */
/************************************************************************/

static bool OGRGeoJSONIsPatchableGeometry(json_object *poJSonGeometry,
                                          json_object *poNativeGeometry,
                                          bool &bOutPatchableCoords,
                                          bool &bOutCompatibleCoords)
{
    if (json_object_get_type(poJSonGeometry) != json_type_object ||
        json_object_get_type(poNativeGeometry) != json_type_object)
    {
        return false;
    }

    json_object *poType = CPL_json_object_object_get(poJSonGeometry, "type");
    json_object *poNativeType =
        CPL_json_object_object_get(poNativeGeometry, "type");
    if (poType == nullptr || poNativeType == nullptr ||
        json_object_get_type(poType) != json_type_string ||
        json_object_get_type(poNativeType) != json_type_string ||
        strcmp(json_object_get_string(poType),
               json_object_get_string(poNativeType)) != 0)
    {
        return false;
    }

    json_object_iter it;
    it.key = nullptr;
    it.val = nullptr;
    it.entry = nullptr;
    json_object_object_foreachC(poNativeGeometry, it)
    {
        if (strcmp(it.key, "coordinates") == 0)
        {
            json_object *poJSonCoordinates =
                CPL_json_object_object_get(poJSonGeometry, "coordinates");
            json_object *poNativeCoordinates = it.val;
            // 0 = Point
            // 1 = LineString or MultiPoint
            // 2 = MultiLineString or Polygon
            // 3 = MultiPolygon
            for (int i = 0; i <= 3; i++)
            {
                if (OGRGeoJSONComputePatchableOrCompatibleArray(
                        poJSonCoordinates, poNativeCoordinates, i,
                        bOutPatchableCoords, bOutCompatibleCoords))
                {
                    return bOutPatchableCoords || bOutCompatibleCoords;
                }
            }
            return false;
        }
        if (strcmp(it.key, "geometries") == 0)
        {
            json_object *poJSonGeometries =
                CPL_json_object_object_get(poJSonGeometry, "geometries");
            json_object *poNativeGeometries = it.val;
            if (json_object_get_type(poJSonGeometries) == json_type_array &&
                json_object_get_type(poNativeGeometries) == json_type_array)
            {
                const auto nLength = json_object_array_length(poJSonGeometries);
                if (nLength == json_object_array_length(poNativeGeometries))
                {
                    for (auto i = decltype(nLength){0}; i < nLength; i++)
                    {
                        json_object *poJSonChild =
                            json_object_array_get_idx(poJSonGeometries, i);
                        json_object *poNativeChild =
                            json_object_array_get_idx(poNativeGeometries, i);
                        if (!OGRGeoJSONIsPatchableGeometry(
                                poJSonChild, poNativeChild, bOutPatchableCoords,
                                bOutCompatibleCoords))
                        {
                            return false;
                        }
                    }
                    return true;
                }
            }
            return false;
        }
    }
    return false;
}

/************************************************************************/
/*                        OGRGeoJSONPatchGeometry()                     */
/************************************************************************/

static void OGRGeoJSONPatchGeometry(json_object *poJSonGeometry,
                                    json_object *poNativeGeometry,
                                    bool bPatchableCoordinates,
                                    const OGRGeoJSONWriteOptions &oOptions)
{
    json_object_iter it;
    it.key = nullptr;
    it.val = nullptr;
    it.entry = nullptr;
    json_object_object_foreachC(poNativeGeometry, it)
    {
        if (strcmp(it.key, "type") == 0 || strcmp(it.key, "bbox") == 0)
        {
            continue;
        }
        if (strcmp(it.key, "coordinates") == 0)
        {
            if (!bPatchableCoordinates &&
                !oOptions.bCanPatchCoordinatesWithNativeData)
            {
                continue;
            }

            json_object *poJSonCoordinates =
                CPL_json_object_object_get(poJSonGeometry, "coordinates");
            json_object *poNativeCoordinates = it.val;
            for (int i = 0; i <= 3; i++)
            {
                if (OGRGeoJSONIsPatchableArray(poJSonCoordinates,
                                               poNativeCoordinates, i))
                {
                    OGRGeoJSONPatchArray(poJSonCoordinates, poNativeCoordinates,
                                         i);
                    break;
                }
            }

            continue;
        }
        if (strcmp(it.key, "geometries") == 0)
        {
            json_object *poJSonGeometries =
                CPL_json_object_object_get(poJSonGeometry, "geometries");
            json_object *poNativeGeometries = it.val;
            const auto nLength = json_object_array_length(poJSonGeometries);
            for (auto i = decltype(nLength){0}; i < nLength; i++)
            {
                json_object *poJSonChild =
                    json_object_array_get_idx(poJSonGeometries, i);
                json_object *poNativeChild =
                    json_object_array_get_idx(poNativeGeometries, i);
                OGRGeoJSONPatchGeometry(poJSonChild, poNativeChild,
                                        bPatchableCoordinates, oOptions);
            }

            continue;
        }

        // See https://tools.ietf.org/html/rfc7946#section-7.1
        if (oOptions.bHonourReservedRFC7946Members &&
            (strcmp(it.key, "geometry") == 0 ||
             strcmp(it.key, "properties") == 0 ||
             strcmp(it.key, "features") == 0))
        {
            continue;
        }

        json_object_object_add(poJSonGeometry, it.key, json_object_get(it.val));
    }
}

/************************************************************************/
/*                           OGRGeoJSONGetBBox                          */
/************************************************************************/

OGREnvelope3D OGRGeoJSONGetBBox(const OGRGeometry *poGeometry,
                                const OGRGeoJSONWriteOptions &oOptions)
{
    OGREnvelope3D sEnvelope;
    poGeometry->getEnvelope(&sEnvelope);

    if (oOptions.bBBOXRFC7946)
    {
        // Heuristics to determine if the geometry was split along the
        // date line.
        const double EPS = 1e-7;
        const OGRwkbGeometryType eType =
            wkbFlatten(poGeometry->getGeometryType());
        const bool bMultiPart =
            OGR_GT_IsSubClassOf(eType, wkbGeometryCollection) &&
            poGeometry->toGeometryCollection()->getNumGeometries() >= 2;
        if (bMultiPart && fabs(sEnvelope.MinX - (-180.0)) < EPS &&
            fabs(sEnvelope.MaxX - 180.0) < EPS)
        {
            // First heuristics (quite safe) when the geometry looks to
            // have been really split at the dateline.
            const auto *poGC = poGeometry->toGeometryCollection();
            double dfWestLimit = -180.0;
            double dfEastLimit = 180.0;
            bool bWestLimitIsInit = false;
            bool bEastLimitIsInit = false;
            for (const auto *poMember : poGC)
            {
                OGREnvelope sEnvelopePart;
                if (poMember->IsEmpty())
                    continue;
                poMember->getEnvelope(&sEnvelopePart);
                const bool bTouchesMinus180 =
                    fabs(sEnvelopePart.MinX - (-180.0)) < EPS;
                const bool bTouchesPlus180 =
                    fabs(sEnvelopePart.MaxX - 180.0) < EPS;
                if (bTouchesMinus180 && !bTouchesPlus180)
                {
                    if (sEnvelopePart.MaxX > dfEastLimit || !bEastLimitIsInit)
                    {
                        bEastLimitIsInit = true;
                        dfEastLimit = sEnvelopePart.MaxX;
                    }
                }
                else if (bTouchesPlus180 && !bTouchesMinus180)
                {
                    if (sEnvelopePart.MinX < dfWestLimit || !bWestLimitIsInit)
                    {
                        bWestLimitIsInit = true;
                        dfWestLimit = sEnvelopePart.MinX;
                    }
                }
                else if (!bTouchesMinus180 && !bTouchesPlus180)
                {
                    if (sEnvelopePart.MinX > 0 &&
                        (sEnvelopePart.MinX < dfWestLimit || !bWestLimitIsInit))
                    {
                        bWestLimitIsInit = true;
                        dfWestLimit = sEnvelopePart.MinX;
                    }
                    else if (sEnvelopePart.MaxX < 0 &&
                             (sEnvelopePart.MaxX > dfEastLimit ||
                              !bEastLimitIsInit))
                    {
                        bEastLimitIsInit = true;
                        dfEastLimit = sEnvelopePart.MaxX;
                    }
                }
            }
            sEnvelope.MinX = dfWestLimit;
            sEnvelope.MaxX = dfEastLimit;
        }
        else if (bMultiPart && sEnvelope.MaxX - sEnvelope.MinX > 180 &&
                 sEnvelope.MinX >= -180 && sEnvelope.MaxX <= 180)
        {
            // More fragile heuristics for a geometry like Alaska
            // (https://github.com/qgis/QGIS/issues/42827) which spans over
            // the antimeridian but does not touch it.
            const auto *poGC = poGeometry->toGeometryCollection();
            double dfWestLimit = std::numeric_limits<double>::infinity();
            double dfEastLimit = -std::numeric_limits<double>::infinity();
            for (const auto *poMember : poGC)
            {
                OGREnvelope sEnvelopePart;
                if (poMember->IsEmpty())
                    continue;
                poMember->getEnvelope(&sEnvelopePart);
                if (sEnvelopePart.MinX > -120 && sEnvelopePart.MaxX < 120)
                {
                    dfWestLimit = std::numeric_limits<double>::infinity();
                    dfEastLimit = -std::numeric_limits<double>::infinity();
                    break;
                }
                if (sEnvelopePart.MinX > 0)
                {
                    dfWestLimit = std::min(dfWestLimit, sEnvelopePart.MinX);
                }
                else
                {
                    CPLAssert(sEnvelopePart.MaxX < 0);
                    dfEastLimit = std::max(dfEastLimit, sEnvelopePart.MaxX);
                }
            }
            if (dfWestLimit != std::numeric_limits<double>::infinity() &&
                dfEastLimit + 360 - dfWestLimit < 180)
            {
                sEnvelope.MinX = dfWestLimit;
                sEnvelope.MaxX = dfEastLimit;
            }
        }
    }

    return sEnvelope;
}

/************************************************************************/
/*                           OGRGeoJSONWriteFeature                     */
/************************************************************************/

json_object *OGRGeoJSONWriteFeature(OGRFeature *poFeature,
                                    const OGRGeoJSONWriteOptions &oOptions)
{
    CPLAssert(nullptr != poFeature);

    bool bWriteBBOX = oOptions.bWriteBBOX;

    json_object *poObj = json_object_new_object();
    CPLAssert(nullptr != poObj);

    json_object_object_add(poObj, "type", json_object_new_string("Feature"));

    /* -------------------------------------------------------------------- */
    /*      Write native JSon data.                                         */
    /* -------------------------------------------------------------------- */
    bool bIdAlreadyWritten = false;
    const char *pszNativeMediaType = poFeature->GetNativeMediaType();
    json_object *poNativeGeom = nullptr;
    bool bHasProperties = true;
    bool bWriteIdIfFoundInAttributes = true;
    if (pszNativeMediaType &&
        EQUAL(pszNativeMediaType, "application/vnd.geo+json"))
    {
        const char *pszNativeData = poFeature->GetNativeData();
        json_object *poNativeJSon = nullptr;
        if (pszNativeData && OGRJSonParse(pszNativeData, &poNativeJSon) &&
            json_object_get_type(poNativeJSon) == json_type_object)
        {
            json_object_iter it;
            it.key = nullptr;
            it.val = nullptr;
            it.entry = nullptr;
            CPLString osNativeData;
            bHasProperties = false;
            json_object_object_foreachC(poNativeJSon, it)
            {
                if (strcmp(it.key, "type") == 0)
                {
                    continue;
                }
                if (strcmp(it.key, "properties") == 0)
                {
                    bHasProperties = true;
                    continue;
                }
                if (strcmp(it.key, "bbox") == 0)
                {
                    bWriteBBOX = true;
                    continue;
                }
                if (strcmp(it.key, "geometry") == 0)
                {
                    poNativeGeom = json_object_get(it.val);
                    continue;
                }
                if (strcmp(it.key, "id") == 0)
                {
                    const auto eType = json_object_get_type(it.val);
                    // See https://tools.ietf.org/html/rfc7946#section-3.2
                    if (oOptions.bHonourReservedRFC7946Members &&
                        !oOptions.bForceIDFieldType &&
                        eType != json_type_string && eType != json_type_int &&
                        eType != json_type_double)
                    {
                        continue;
                    }

                    bIdAlreadyWritten = true;

                    if (it.val && oOptions.bForceIDFieldType &&
                        oOptions.eForcedIDFieldType == OFTInteger64)
                    {
                        if (eType != json_type_int)
                        {
                            json_object_object_add(
                                poObj, it.key,
                                json_object_new_int64(CPLAtoGIntBig(
                                    json_object_get_string(it.val))));
                            bWriteIdIfFoundInAttributes = false;
                            continue;
                        }
                    }
                    else if (it.val && oOptions.bForceIDFieldType &&
                             oOptions.eForcedIDFieldType == OFTString)
                    {
                        if (eType != json_type_string)
                        {
                            json_object_object_add(
                                poObj, it.key,
                                json_object_new_string(
                                    json_object_get_string(it.val)));
                            bWriteIdIfFoundInAttributes = false;
                            continue;
                        }
                    }

                    if (it.val != nullptr)
                    {
                        int nIdx =
                            poFeature->GetDefnRef()->GetFieldIndexCaseSensitive(
                                "id");
                        if (eType == json_type_string && nIdx >= 0 &&
                            poFeature->GetFieldDefnRef(nIdx)->GetType() ==
                                OFTString &&
                            strcmp(json_object_get_string(it.val),
                                   poFeature->GetFieldAsString(nIdx)) == 0)
                        {
                            bWriteIdIfFoundInAttributes = false;
                        }
                        else if (eType == json_type_int && nIdx >= 0 &&
                                 (poFeature->GetFieldDefnRef(nIdx)->GetType() ==
                                      OFTInteger ||
                                  poFeature->GetFieldDefnRef(nIdx)->GetType() ==
                                      OFTInteger64) &&
                                 json_object_get_int64(it.val) ==
                                     poFeature->GetFieldAsInteger64(nIdx))
                        {
                            bWriteIdIfFoundInAttributes = false;
                        }
                    }
                }

                // See https://tools.ietf.org/html/rfc7946#section-7.1
                if (oOptions.bHonourReservedRFC7946Members &&
                    (strcmp(it.key, "coordinates") == 0 ||
                     strcmp(it.key, "geometries") == 0 ||
                     strcmp(it.key, "features") == 0))
                {
                    continue;
                }

                json_object_object_add(poObj, it.key, json_object_get(it.val));
            }
            json_object_put(poNativeJSon);
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Write FID if available                                          */
    /* -------------------------------------------------------------------- */
    OGRGeoJSONWriteId(poFeature, poObj, bIdAlreadyWritten, oOptions);

    /* -------------------------------------------------------------------- */
    /*      Write feature attributes to GeoJSON "properties" object.        */
    /* -------------------------------------------------------------------- */
    if (bHasProperties)
    {
        json_object *poObjProps = OGRGeoJSONWriteAttributes(
            poFeature, bWriteIdIfFoundInAttributes, oOptions);
        json_object_object_add(poObj, "properties", poObjProps);
    }

    /* -------------------------------------------------------------------- */
    /*      Write feature geometry to GeoJSON "geometry" object.            */
    /*      Null geometries are allowed, according to the GeoJSON Spec.     */
    /* -------------------------------------------------------------------- */
    json_object *poObjGeom = nullptr;

    OGRGeometry *poGeometry = poFeature->GetGeometryRef();
    if (nullptr != poGeometry)
    {
        poObjGeom = OGRGeoJSONWriteGeometry(poGeometry, oOptions);

        if (bWriteBBOX && !poGeometry->IsEmpty())
        {
            OGREnvelope3D sEnvelope = OGRGeoJSONGetBBox(poGeometry, oOptions);

            json_object *poObjBBOX = json_object_new_array();
            json_object_array_add(
                poObjBBOX, json_object_new_coord(sEnvelope.MinX, 1, oOptions));
            json_object_array_add(
                poObjBBOX, json_object_new_coord(sEnvelope.MinY, 2, oOptions));
            if (wkbHasZ(poGeometry->getGeometryType()))
                json_object_array_add(
                    poObjBBOX,
                    json_object_new_coord(sEnvelope.MinZ, 3, oOptions));
            json_object_array_add(
                poObjBBOX, json_object_new_coord(sEnvelope.MaxX, 1, oOptions));
            json_object_array_add(
                poObjBBOX, json_object_new_coord(sEnvelope.MaxY, 2, oOptions));
            if (wkbHasZ(poGeometry->getGeometryType()))
                json_object_array_add(
                    poObjBBOX,
                    json_object_new_coord(sEnvelope.MaxZ, 3, oOptions));

            json_object_object_add(poObj, "bbox", poObjBBOX);
        }

        bool bOutPatchableCoords = false;
        bool bOutCompatibleCoords = false;
        if (OGRGeoJSONIsPatchableGeometry(poObjGeom, poNativeGeom,
                                          bOutPatchableCoords,
                                          bOutCompatibleCoords))
        {
            OGRGeoJSONPatchGeometry(poObjGeom, poNativeGeom,
                                    bOutPatchableCoords, oOptions);
        }
    }

    json_object_object_add(poObj, "geometry", poObjGeom);

    if (poNativeGeom != nullptr)
        json_object_put(poNativeGeom);

    return poObj;
}

/************************************************************************/
/*                        OGRGeoJSONWriteId                            */
/************************************************************************/

void OGRGeoJSONWriteId(const OGRFeature *poFeature, json_object *poObj,
                       bool bIdAlreadyWritten,
                       const OGRGeoJSONWriteOptions &oOptions)
{
    if (!oOptions.osIDField.empty())
    {
        int nIdx = poFeature->GetDefnRef()->GetFieldIndexCaseSensitive(
            oOptions.osIDField);
        if (nIdx >= 0)
        {
            if ((oOptions.bForceIDFieldType &&
                 oOptions.eForcedIDFieldType == OFTInteger64) ||
                (!oOptions.bForceIDFieldType &&
                 (poFeature->GetFieldDefnRef(nIdx)->GetType() == OFTInteger ||
                  poFeature->GetFieldDefnRef(nIdx)->GetType() == OFTInteger64)))
            {
                json_object_object_add(
                    poObj, "id",
                    json_object_new_int64(
                        poFeature->GetFieldAsInteger64(nIdx)));
            }
            else
            {
                json_object_object_add(
                    poObj, "id",
                    json_object_new_string(poFeature->GetFieldAsString(nIdx)));
            }
        }
    }
    else if (poFeature->GetFID() != OGRNullFID && !bIdAlreadyWritten)
    {
        if (oOptions.bForceIDFieldType &&
            oOptions.eForcedIDFieldType == OFTString)
        {
            json_object_object_add(poObj, "id",
                                   json_object_new_string(CPLSPrintf(
                                       CPL_FRMT_GIB, poFeature->GetFID())));
        }
        else
        {
            json_object_object_add(poObj, "id",
                                   json_object_new_int64(poFeature->GetFID()));
        }
    }
}

/************************************************************************/
/*                        OGRGeoJSONWriteAttributes                     */
/************************************************************************/

json_object *OGRGeoJSONWriteAttributes(OGRFeature *poFeature,
                                       bool bWriteIdIfFoundInAttributes,
                                       const OGRGeoJSONWriteOptions &oOptions)
{
    CPLAssert(nullptr != poFeature);

    json_object *poObjProps = json_object_new_object();
    CPLAssert(nullptr != poObjProps);

    const OGRFeatureDefn *poDefn = poFeature->GetDefnRef();

    const int nIDField =
        !oOptions.osIDField.empty()
            ? poDefn->GetFieldIndexCaseSensitive(oOptions.osIDField)
            : -1;

    constexpr int MAX_SIGNIFICANT_DIGITS_FLOAT32 = 8;
    const int nFloat32SignificantDigits =
        oOptions.nSignificantFigures >= 0
            ? std::min(oOptions.nSignificantFigures,
                       MAX_SIGNIFICANT_DIGITS_FLOAT32)
            : MAX_SIGNIFICANT_DIGITS_FLOAT32;

    const int nFieldCount = poDefn->GetFieldCount();

    json_object *poNativeObjProp = nullptr;
    json_object *poProperties = nullptr;

    // Scan the fields to determine if there is a chance of
    // mixed types and we can use native media
    bool bUseNativeMedia{false};

    if (poFeature->GetNativeMediaType() &&
        strcmp(poFeature->GetNativeMediaType(), "application/vnd.geo+json") ==
            0 &&
        poFeature->GetNativeData())
    {
        for (int nField = 0; nField < nFieldCount; ++nField)
        {
            if (poDefn->GetFieldDefn(nField)->GetSubType() == OFSTJSON)
            {
                if (OGRJSonParse(poFeature->GetNativeData(), &poNativeObjProp,
                                 false))
                {
                    poProperties = OGRGeoJSONFindMemberByName(poNativeObjProp,
                                                              "properties");
                    bUseNativeMedia = poProperties != nullptr;
                }
                break;
            }
        }
    }

    for (int nField = 0; nField < nFieldCount; ++nField)
    {
        if (!poFeature->IsFieldSet(nField) || nField == nIDField)
        {
            continue;
        }

        const OGRFieldDefn *poFieldDefn = poDefn->GetFieldDefn(nField);
        CPLAssert(nullptr != poFieldDefn);
        const OGRFieldType eType = poFieldDefn->GetType();
        const OGRFieldSubType eSubType = poFieldDefn->GetSubType();

        if (!bWriteIdIfFoundInAttributes &&
            strcmp(poFieldDefn->GetNameRef(), "id") == 0)
        {
            continue;
        }

        json_object *poObjProp = nullptr;

        if (poFeature->IsFieldNull(nField))
        {
            // poObjProp = NULL;
        }
        else if (OFTInteger == eType)
        {
            if (eSubType == OFSTBoolean)
                poObjProp = json_object_new_boolean(
                    poFeature->GetFieldAsInteger(nField));
            else
                poObjProp =
                    json_object_new_int(poFeature->GetFieldAsInteger(nField));
        }
        else if (OFTInteger64 == eType)
        {
            if (eSubType == OFSTBoolean)
                poObjProp = json_object_new_boolean(static_cast<json_bool>(
                    poFeature->GetFieldAsInteger64(nField)));
            else
                poObjProp = json_object_new_int64(
                    poFeature->GetFieldAsInteger64(nField));
        }
        else if (OFTReal == eType)
        {
            const double val = poFeature->GetFieldAsDouble(nField);
            if (!std::isfinite(val))
            {
                if (!oOptions.bAllowNonFiniteValues)
                {
                    CPLErrorOnce(CE_Warning, CPLE_AppDefined,
                                 "NaN of Infinity value found. Skipped");
                    continue;
                }
            }
            if (eSubType == OFSTFloat32)
            {
                poObjProp = json_object_new_float_with_significant_figures(
                    static_cast<float>(val), nFloat32SignificantDigits);
            }
            else
            {
                poObjProp = json_object_new_double_with_significant_figures(
                    val, oOptions.nSignificantFigures);
            }
        }
        else if (OFTString == eType)
        {
            const char *pszStr = poFeature->GetFieldAsString(nField);
            const size_t nLen = strlen(pszStr);

            if (eSubType == OFSTJSON ||
                (oOptions.bAutodetectJsonStrings &&
                 ((pszStr[0] == '{' && pszStr[nLen - 1] == '}') ||
                  (pszStr[0] == '[' && pszStr[nLen - 1] == ']'))))
            {
                if (bUseNativeMedia)
                {
                    if (json_object *poProperty = OGRGeoJSONFindMemberByName(
                            poProperties, poFieldDefn->GetNameRef()))
                    {
                        const char *pszProp{json_object_get_string(poProperty)};
                        if (pszProp && strcmp(pszProp, pszStr) == 0)
                        {
                            poObjProp = json_object_get(poProperty);
                        }
                    }
                }

                if (poObjProp == nullptr)
                {
                    if ((pszStr[0] == '{' && pszStr[nLen - 1] == '}') ||
                        (pszStr[0] == '[' && pszStr[nLen - 1] == ']'))
                    {
                        OGRJSonParse(pszStr, &poObjProp, false);
                    }
                }
            }

            if (poObjProp == nullptr)
                poObjProp = json_object_new_string(pszStr);
        }
        else if (OFTIntegerList == eType)
        {
            int nSize = 0;
            const int *panList =
                poFeature->GetFieldAsIntegerList(nField, &nSize);
            poObjProp = json_object_new_array();
            for (int i = 0; i < nSize; i++)
            {
                if (eSubType == OFSTBoolean)
                    json_object_array_add(poObjProp,
                                          json_object_new_boolean(panList[i]));
                else
                    json_object_array_add(poObjProp,
                                          json_object_new_int(panList[i]));
            }
        }
        else if (OFTInteger64List == eType)
        {
            int nSize = 0;
            const GIntBig *panList =
                poFeature->GetFieldAsInteger64List(nField, &nSize);
            poObjProp = json_object_new_array();
            for (int i = 0; i < nSize; i++)
            {
                if (eSubType == OFSTBoolean)
                    json_object_array_add(
                        poObjProp, json_object_new_boolean(
                                       static_cast<json_bool>(panList[i])));
                else
                    json_object_array_add(poObjProp,
                                          json_object_new_int64(panList[i]));
            }
        }
        else if (OFTRealList == eType)
        {
            int nSize = 0;
            const double *padfList =
                poFeature->GetFieldAsDoubleList(nField, &nSize);
            poObjProp = json_object_new_array();
            for (int i = 0; i < nSize; i++)
            {
                if (eSubType == OFSTFloat32)
                {
                    json_object_array_add(
                        poObjProp,
                        json_object_new_float_with_significant_figures(
                            static_cast<float>(padfList[i]),
                            nFloat32SignificantDigits));
                }
                else
                {
                    json_object_array_add(
                        poObjProp,
                        json_object_new_double_with_significant_figures(
                            padfList[i], oOptions.nSignificantFigures));
                }
            }
        }
        else if (OFTStringList == eType)
        {
            char **papszStringList = poFeature->GetFieldAsStringList(nField);
            poObjProp = json_object_new_array();
            for (int i = 0; papszStringList && papszStringList[i]; i++)
            {
                json_object_array_add(
                    poObjProp, json_object_new_string(papszStringList[i]));
            }
        }
        else if (OFTDateTime == eType || OFTDate == eType)
        {
            char *pszDT = OGRGetXMLDateTime(poFeature->GetRawFieldRef(nField));
            if (eType == OFTDate)
            {
                char *pszT = strchr(pszDT, 'T');
                if (pszT)
                    *pszT = 0;
            }
            poObjProp = json_object_new_string(pszDT);
            CPLFree(pszDT);
        }
        else
        {
            poObjProp =
                json_object_new_string(poFeature->GetFieldAsString(nField));
        }

        json_object_object_add(poObjProps, poFieldDefn->GetNameRef(),
                               poObjProp);
    }

    if (bUseNativeMedia)
    {
        json_object_put(poNativeObjProp);
    }

    return poObjProps;
}

/************************************************************************/
/*                          GetLinearCollection()                       */
/************************************************************************/

static std::unique_ptr<OGRGeometry>
GetLinearCollection(const OGRGeometryCollection *poGeomColl)
{
    auto poFlatGeom = std::make_unique<OGRGeometryCollection>();
    for (const auto *poSubGeom : *poGeomColl)
    {
        if (wkbFlatten(poSubGeom->getGeometryType()) == wkbGeometryCollection)
        {
            poFlatGeom->addGeometry(
                GetLinearCollection(poSubGeom->toGeometryCollection()));
        }
        else
        {
            auto poNewGeom = OGRGeometryFactory::forceTo(
                poSubGeom->clone(),
                OGR_GT_GetLinear(poSubGeom->getGeometryType()));
            if (poNewGeom)
                poFlatGeom->addGeometryDirectly(poNewGeom);
        }
    }
    return poFlatGeom;
}

/************************************************************************/
/*                           OGRGeoJSONWriteGeometry                    */
/************************************************************************/

json_object *OGRGeoJSONWriteGeometry(const OGRGeometry *poGeometry,
                                     const OGRGeoJSONWriteOptions &oOptions)
{
    if (poGeometry == nullptr)
    {
        CPLAssert(false);
        return nullptr;
    }

    if (!oOptions.bAllowCurve && poGeometry->hasCurveGeometry(true))
    {
        const OGRwkbGeometryType eTargetType =
            OGR_GT_GetLinear(poGeometry->getGeometryType());
        std::unique_ptr<OGRGeometry> poFlatGeom;
        if (wkbFlatten(eTargetType) == wkbGeometryCollection)
        {
            poFlatGeom =
                GetLinearCollection(poGeometry->toGeometryCollection());
        }
        else
        {
            poFlatGeom.reset(
                OGRGeometryFactory::forceTo(poGeometry->clone(), eTargetType));
        }
        return OGRGeoJSONWriteGeometry(poFlatGeom.get(), oOptions);
    }

    OGRwkbGeometryType eFType = wkbFlatten(poGeometry->getGeometryType());
    // For point empty, return a null geometry. For other empty geometry types,
    // we will generate an empty coordinate array, which is probably also
    // borderline.
    if (eFType == wkbPoint && poGeometry->IsEmpty())
    {
        return nullptr;
    }

    std::unique_ptr<OGRGeometry> poTmpGeom;  // keep in that scope
    if (eFType == wkbCircularString)
    {
        auto poCS = poGeometry->toCircularString();
        const int nNumPoints = poCS->getNumPoints();
        constexpr int MAX_POINTS_PER_CC = 11;
        if (nNumPoints > MAX_POINTS_PER_CC)
        {
            auto poCC = std::make_unique<OGRCompoundCurve>();
            auto poSubCS = std::make_unique<OGRCircularString>();
            for (int i = 0; i < nNumPoints; ++i)
            {
                OGRPoint oPoint;
                poCS->getPoint(i, &oPoint);
                poSubCS->addPoint(&oPoint);
                if (poSubCS->getNumPoints() == MAX_POINTS_PER_CC)
                {
                    poCC->addCurve(std::move(poSubCS));
                    poSubCS = std::make_unique<OGRCircularString>();
                    poSubCS->addPoint(&oPoint);
                }
            }
            if (poSubCS->getNumPoints() > 1)
                poCC->addCurve(std::move(poSubCS));
            poTmpGeom = std::move(poCC);
            poGeometry = poTmpGeom.get();
            eFType = wkbCompoundCurve;
        }
    }

    json_object *poObj = json_object_new_object();
    CPLAssert(nullptr != poObj);

    /* -------------------------------------------------------------------- */
    /*      Build "type" member of GeoJSON "geometry" object.               */
    /* -------------------------------------------------------------------- */

    const char *pszName = OGRGeoJSONGetGeometryName(poGeometry);
    json_object_object_add(poObj, "type", json_object_new_string(pszName));

    /* -------------------------------------------------------------------- */
    /*      Build "coordinates" member of GeoJSON "geometry" object.        */
    /* -------------------------------------------------------------------- */
    json_object *poObjGeom = nullptr;

    if (eFType == wkbGeometryCollection || eFType == wkbMultiCurve ||
        eFType == wkbMultiSurface)
    {
        poObjGeom = OGRGeoJSONWriteGeometryCollection(
            poGeometry->toGeometryCollection(), oOptions);
        json_object_object_add(poObj, "geometries", poObjGeom);
    }
    else if (eFType == wkbCompoundCurve)
    {
        poObjGeom = OGRGeoJSONWriteCompoundCurve(poGeometry->toCompoundCurve(),
                                                 oOptions);
        json_object_object_add(poObj, "geometries", poObjGeom);
    }
    else if (eFType == wkbCurvePolygon)
    {
        poObjGeom =
            OGRGeoJSONWriteCurvePolygon(poGeometry->toCurvePolygon(), oOptions);
        json_object_object_add(poObj, "geometries", poObjGeom);
    }
    else
    {
        if (wkbPoint == eFType)
            poObjGeom = OGRGeoJSONWritePoint(poGeometry->toPoint(), oOptions);
        else if (wkbLineString == eFType || wkbCircularString == eFType)
            poObjGeom = OGRGeoJSONWriteSimpleCurve(poGeometry->toSimpleCurve(),
                                                   oOptions);
        else if (wkbPolygon == eFType)
            poObjGeom =
                OGRGeoJSONWritePolygon(poGeometry->toPolygon(), oOptions);
        else if (wkbMultiPoint == eFType)
            poObjGeom =
                OGRGeoJSONWriteMultiPoint(poGeometry->toMultiPoint(), oOptions);
        else if (wkbMultiLineString == eFType)
            poObjGeom = OGRGeoJSONWriteMultiLineString(
                poGeometry->toMultiLineString(), oOptions);
        else if (wkbMultiPolygon == eFType)
            poObjGeom = OGRGeoJSONWriteMultiPolygon(
                poGeometry->toMultiPolygon(), oOptions);
        else
        {
            CPLError(
                CE_Failure, CPLE_NotSupported,
                "OGR geometry type unsupported as a GeoJSON geometry detected. "
                "Feature gets NULL geometry assigned.");
        }

        if (poObjGeom != nullptr)
        {
            json_object_object_add(poObj, "coordinates", poObjGeom);
        }
        else
        {
            json_object_put(poObj);
            poObj = nullptr;
        }
    }

    return poObj;
}

/************************************************************************/
/*                           OGRGeoJSONWritePoint                       */
/************************************************************************/

json_object *OGRGeoJSONWritePoint(const OGRPoint *poPoint,
                                  const OGRGeoJSONWriteOptions &oOptions)
{
    CPLAssert(nullptr != poPoint);

    json_object *poObj = nullptr;

    // Generate "coordinates" object
    if (!poPoint->IsEmpty())
    {
        if (oOptions.bAllowMeasure && poPoint->IsMeasured())
        {
            if (poPoint->Is3D())
            {
                poObj = OGRGeoJSONWriteCoords(poPoint->getX(), poPoint->getY(),
                                              poPoint->getZ(), poPoint->getM(),
                                              oOptions);
            }
            else
            {
                poObj = OGRGeoJSONWriteCoords(poPoint->getX(), poPoint->getY(),
                                              std::nullopt, poPoint->getM(),
                                              oOptions);
            }
        }
        else if (poPoint->Is3D())
        {
            poObj =
                OGRGeoJSONWriteCoords(poPoint->getX(), poPoint->getY(),
                                      poPoint->getZ(), std::nullopt, oOptions);
        }
        else
        {
            poObj = OGRGeoJSONWriteCoords(poPoint->getX(), poPoint->getY(),
                                          std::nullopt, std::nullopt, oOptions);
        }
    }

    return poObj;
}

/************************************************************************/
/*                           OGRGeoJSONWriteSimpleCurve                  */
/************************************************************************/

json_object *OGRGeoJSONWriteSimpleCurve(const OGRSimpleCurve *poLine,
                                        const OGRGeoJSONWriteOptions &oOptions)
{
    CPLAssert(nullptr != poLine);

    // Generate "coordinates" object for 2D or 3D dimension.
    json_object *poObj = OGRGeoJSONWriteLineCoords(poLine, oOptions);

    return poObj;
}

/************************************************************************/
/*                           OGRGeoJSONWritePolygon                     */
/************************************************************************/

json_object *OGRGeoJSONWritePolygon(const OGRPolygon *poPolygon,
                                    const OGRGeoJSONWriteOptions &oOptions)
{
    CPLAssert(nullptr != poPolygon);

    // Generate "coordinates" array object.
    json_object *poObj = json_object_new_array();

    bool bExteriorRing = true;
    for (const auto *poRing : *poPolygon)
    {
        json_object *poObjRing =
            OGRGeoJSONWriteRingCoords(poRing, bExteriorRing, oOptions);
        bExteriorRing = false;
        if (poObjRing == nullptr)
        {
            json_object_put(poObj);
            return nullptr;
        }
        json_object_array_add(poObj, poObjRing);
    }

    return poObj;
}

/************************************************************************/
/*                           OGRGeoJSONWriteMultiPoint                  */
/************************************************************************/

json_object *OGRGeoJSONWriteMultiPoint(const OGRMultiPoint *poGeometry,
                                       const OGRGeoJSONWriteOptions &oOptions)
{
    CPLAssert(nullptr != poGeometry);

    // Generate "coordinates" object
    json_object *poObj = json_object_new_array();

    for (const auto *poPoint : poGeometry)
    {
        json_object *poObjPoint = OGRGeoJSONWritePoint(poPoint, oOptions);
        if (poObjPoint == nullptr)
        {
            json_object_put(poObj);
            return nullptr;
        }

        json_object_array_add(poObj, poObjPoint);
    }

    return poObj;
}

/************************************************************************/
/*                           OGRGeoJSONWriteMultiLineString             */
/************************************************************************/

json_object *
OGRGeoJSONWriteMultiLineString(const OGRMultiLineString *poGeometry,
                               const OGRGeoJSONWriteOptions &oOptions)
{
    CPLAssert(nullptr != poGeometry);

    // Generate "coordinates" object
    json_object *poObj = json_object_new_array();

    for (const auto *poLine : poGeometry)
    {
        json_object *poObjLine = OGRGeoJSONWriteSimpleCurve(poLine, oOptions);
        if (poObjLine == nullptr)
        {
            json_object_put(poObj);
            return nullptr;
        }

        json_object_array_add(poObj, poObjLine);
    }

    return poObj;
}

/************************************************************************/
/*                           OGRGeoJSONWriteMultiPolygon                */
/************************************************************************/

json_object *OGRGeoJSONWriteMultiPolygon(const OGRMultiPolygon *poGeometry,
                                         const OGRGeoJSONWriteOptions &oOptions)
{
    CPLAssert(nullptr != poGeometry);

    // Generate "coordinates" object
    json_object *poObj = json_object_new_array();

    for (const auto *poPoly : poGeometry)
    {
        json_object *poObjPoly = OGRGeoJSONWritePolygon(poPoly, oOptions);
        if (poObjPoly == nullptr)
        {
            json_object_put(poObj);
            return nullptr;
        }

        json_object_array_add(poObj, poObjPoly);
    }

    return poObj;
}

/************************************************************************/
/*                   OGRGeoJSONWriteCollectionGeneric()                 */
/************************************************************************/

template <class T>
static json_object *
OGRGeoJSONWriteCollectionGeneric(const T *poGeometry,
                                 const OGRGeoJSONWriteOptions &oOptions)
{
    CPLAssert(nullptr != poGeometry);

    /* Generate "geometries" object. */
    json_object *poObj = json_object_new_array();

    for (const OGRGeometry *poGeom : *poGeometry)
    {
        json_object *poObjGeom = OGRGeoJSONWriteGeometry(poGeom, oOptions);
        if (poObjGeom == nullptr)
        {
            json_object_put(poObj);
            return nullptr;
        }

        json_object_array_add(poObj, poObjGeom);
    }

    return poObj;
}

/************************************************************************/
/*                           OGRGeoJSONWriteGeometryCollection          */
/************************************************************************/

json_object *
OGRGeoJSONWriteGeometryCollection(const OGRGeometryCollection *poGeometry,
                                  const OGRGeoJSONWriteOptions &oOptions)
{
    return OGRGeoJSONWriteCollectionGeneric(poGeometry, oOptions);
}

/************************************************************************/
/*                       OGRGeoJSONWriteCompoundCurve                   */
/************************************************************************/

json_object *
OGRGeoJSONWriteCompoundCurve(const OGRCompoundCurve *poGeometry,
                             const OGRGeoJSONWriteOptions &oOptions)
{
    return OGRGeoJSONWriteCollectionGeneric(poGeometry, oOptions);
}

/************************************************************************/
/*                       OGRGeoJSONWriteCurvePolygon                    */
/************************************************************************/

json_object *OGRGeoJSONWriteCurvePolygon(const OGRCurvePolygon *poGeometry,
                                         const OGRGeoJSONWriteOptions &oOptions)
{
    return OGRGeoJSONWriteCollectionGeneric(poGeometry, oOptions);
}

/************************************************************************/
/*                           OGRGeoJSONWriteCoords                      */
/************************************************************************/

json_object *OGRGeoJSONWriteCoords(double dfX, double dfY,
                                   std::optional<double> dfZ,
                                   std::optional<double> dfM,
                                   const OGRGeoJSONWriteOptions &oOptions)
{
    json_object *poObjCoords = nullptr;
    if (!std::isfinite(dfX) || !std::isfinite(dfY) ||
        (dfZ && !std::isfinite(*dfZ)) || (dfM && !std::isfinite(*dfM)))
    {
        CPLError(CE_Warning, CPLE_AppDefined,
                 "Infinite or NaN coordinate encountered");
        return nullptr;
    }
    poObjCoords = json_object_new_array();
    json_object_array_add(poObjCoords, json_object_new_coord(dfX, 1, oOptions));
    json_object_array_add(poObjCoords, json_object_new_coord(dfY, 2, oOptions));
    int nIdx = 3;
    if (dfZ)
    {
        json_object_array_add(poObjCoords,
                              json_object_new_coord(*dfZ, nIdx, oOptions));
        nIdx++;
    }
    if (dfM)
    {
        json_object_array_add(poObjCoords,
                              json_object_new_coord(*dfM, nIdx, oOptions));
    }

    return poObjCoords;
}

/************************************************************************/
/*                           OGRGeoJSONWriteLineCoords                  */
/************************************************************************/

json_object *OGRGeoJSONWriteLineCoords(const OGRSimpleCurve *poLine,
                                       const OGRGeoJSONWriteOptions &oOptions)
{
    json_object *poObjCoords = json_object_new_array();

    const int nCount = poLine->getNumPoints();
    const auto bHasZ = poLine->Is3D();
    const auto bHasM = oOptions.bAllowMeasure && poLine->IsMeasured();
    for (int i = 0; i < nCount; ++i)
    {
        json_object *poObjPoint;
        if (bHasZ)
        {
            if (bHasM)
            {
                poObjPoint = OGRGeoJSONWriteCoords(
                    poLine->getX(i), poLine->getY(i), poLine->getZ(i),
                    poLine->getM(i), oOptions);
            }
            else
            {
                poObjPoint = OGRGeoJSONWriteCoords(
                    poLine->getX(i), poLine->getY(i), poLine->getZ(i),
                    std::nullopt, oOptions);
            }
        }
        else if (bHasM)
        {
            poObjPoint =
                OGRGeoJSONWriteCoords(poLine->getX(i), poLine->getY(i),
                                      std::nullopt, poLine->getM(i), oOptions);
        }
        else
        {
            poObjPoint =
                OGRGeoJSONWriteCoords(poLine->getX(i), poLine->getY(i),
                                      std::nullopt, std::nullopt, oOptions);
        }
        if (poObjPoint == nullptr)
        {
            json_object_put(poObjCoords);
            return nullptr;
        }
        json_object_array_add(poObjCoords, poObjPoint);
    }

    return poObjCoords;
}

/************************************************************************/
/*                        OGRGeoJSONWriteRingCoords                     */
/************************************************************************/

json_object *OGRGeoJSONWriteRingCoords(const OGRLinearRing *poLine,
                                       bool bIsExteriorRing,
                                       const OGRGeoJSONWriteOptions &oOptions)
{
    json_object *poObjCoords = json_object_new_array();

    const bool bInvertOrder = oOptions.bPolygonRightHandRule &&
                              ((bIsExteriorRing && poLine->isClockwise()) ||
                               (!bIsExteriorRing && !poLine->isClockwise()));

    const int nCount = poLine->getNumPoints();
    const auto bHasZ = poLine->Is3D();
    const auto bHasM = oOptions.bAllowMeasure && poLine->IsMeasured();
    for (int i = 0; i < nCount; ++i)
    {
        const int nIdx = (bInvertOrder) ? nCount - 1 - i : i;
        json_object *poObjPoint;
        if (bHasZ)
        {
            if (bHasM)
            {
                poObjPoint = OGRGeoJSONWriteCoords(
                    poLine->getX(nIdx), poLine->getY(nIdx), poLine->getZ(nIdx),
                    poLine->getM(nIdx), oOptions);
            }
            else
            {
                poObjPoint = OGRGeoJSONWriteCoords(
                    poLine->getX(nIdx), poLine->getY(nIdx), poLine->getZ(nIdx),
                    std::nullopt, oOptions);
            }
        }
        else if (bHasM)
        {
            poObjPoint = OGRGeoJSONWriteCoords(poLine->getX(nIdx),
                                               poLine->getY(nIdx), std::nullopt,
                                               poLine->getM(nIdx), oOptions);
        }
        else
        {
            poObjPoint =
                OGRGeoJSONWriteCoords(poLine->getX(nIdx), poLine->getY(nIdx),
                                      std::nullopt, std::nullopt, oOptions);
        }
        if (poObjPoint == nullptr)
        {
            json_object_put(poObjCoords);
            return nullptr;
        }
        json_object_array_add(poObjCoords, poObjPoint);
    }

    return poObjCoords;
}

/************************************************************************/
/*             OGR_json_float_with_significant_figures_to_string()      */
/************************************************************************/

static int OGR_json_float_with_significant_figures_to_string(
    struct json_object *jso, struct printbuf *pb, int /* level */,
    int /* flags */)
{
    char szBuffer[75] = {};
    int nSize = 0;
    const float fVal = static_cast<float>(json_object_get_double(jso));
    if (std::isnan(fVal))
        nSize = CPLsnprintf(szBuffer, sizeof(szBuffer), "NaN");
    else if (std::isinf(fVal))
    {
        if (fVal > 0)
            nSize = CPLsnprintf(szBuffer, sizeof(szBuffer), "Infinity");
        else
            nSize = CPLsnprintf(szBuffer, sizeof(szBuffer), "-Infinity");
    }
    else
    {
        const void *userData =
#if (!defined(JSON_C_VERSION_NUM)) || (JSON_C_VERSION_NUM < JSON_C_VER_013)
            jso->_userdata;
#else
            json_object_get_userdata(jso);
#endif
        const uintptr_t nSignificantFigures =
            reinterpret_cast<uintptr_t>(userData);
        const bool bSignificantFiguresIsNegative =
            (nSignificantFigures >> (8 * sizeof(nSignificantFigures) - 1)) != 0;
        const int nInitialSignificantFigures =
            bSignificantFiguresIsNegative
                ? 8
                : static_cast<int>(nSignificantFigures);
        nSize = OGRFormatFloat(szBuffer, sizeof(szBuffer), fVal,
                               nInitialSignificantFigures, 'g');
    }

    return printbuf_memappend(pb, szBuffer, nSize);
}

/************************************************************************/
/*              json_object_new_float_with_significant_figures()        */
/************************************************************************/

json_object *
json_object_new_float_with_significant_figures(float fVal,
                                               int nSignificantFigures)
{
    json_object *jso = json_object_new_double(double(fVal));
    json_object_set_serializer(
        jso, OGR_json_float_with_significant_figures_to_string,
        reinterpret_cast<void *>(static_cast<uintptr_t>(nSignificantFigures)),
        nullptr);
    return jso;
}

/*! @endcond */

/************************************************************************/
/*                           OGR_G_ExportToJson                         */
/************************************************************************/

/**
 * \brief Convert a geometry into GeoJSON format.
 *
 * The returned string should be freed with CPLFree() when no longer required.
 *
 * This method is the same as the C++ method OGRGeometry::exportToJson().
 *
 * @param hGeometry handle to the geometry.
 * @return A GeoJSON fragment or NULL in case of error.
 */

char *OGR_G_ExportToJson(OGRGeometryH hGeometry)
{
    return OGR_G_ExportToJsonEx(hGeometry, nullptr);
}

/************************************************************************/
/*                           OGR_G_ExportToJsonEx                       */
/************************************************************************/

/**
 * \brief Convert a geometry into GeoJSON-style format.
 *
 * The returned string should be freed with CPLFree() when no longer required.
 *
 * If setting ALLOW_CURVE=YES and ALLOW_MEASURE=YES, the result is compatible
 * of JSON-FG geometries. If there is a SRS attached to the geometry, and the
 * geometry is aimed at being stored in the "place" member of JSON-FG features,
 * then the COORDINATE_ORDER option must be set to AUTHORITY_COMPLIANT.
 *
 * The following options are supported :
 * <ul>
 * <li>COORDINATE_PRECISION=number: maximum number of figures after decimal
 * separator to write in coordinates.</li>
 * <li>XY_COORD_PRECISION=integer: number of decimal figures for X,Y coordinates
 * (added in GDAL 3.9)</li>
 * <li>Z_COORD_PRECISION=integer: number of decimal figures for Z coordinates
 * (added in GDAL 3.9)</li>
 * <li>SIGNIFICANT_FIGURES=number: maximum number of significant figures.</li>
 * <li>ALLOW_CURVE=YES/NO: whether curve geometries are allowed. When set to NO
 * (its default value), they are converted to linear geometries first.
 * Curves are not allowed in GeoJSON, but they are in JSON-FG geometries.
 * (added in GDAL 3.12.1)</li>
 * <li>ALLOW_MEASURE=YES/NO: whether the measure (M) component of geometries is
 * allowed. When set to NO (its default value), it is dropped when present.
 * Measures are not allowed in GeoJSON, but they are in JSON-FG geometries.
 * (added in GDAL 3.12.1)</li>
 * <li>COORDINATE_ORDER=TRADITIONAL_GIS_ORDER/AUTHORITY_COMPLIANT (added in GDAL 3.12.1):
 * When a SRS is attached to the geometry, and AUTHORITY_COMPLIANT is used,
 * the coordinates will be emitted in the order of the official SRS definition.
 * When using TRADITIONAL_GIS_ORDER (the default), coordinates are emitted in
 * longitude/easting first, latitude/northing second.
 * When no SRS is attached, coordinates are emitted in the order they are set
 * in the geometry.
 * When this function is used to emit JSON-FG geometries stored in the "place"
 * member, this option must be set to AUTHORITY_COMPLIANT if there is a SRS
 * attached to the geometry.
 * </li>
 * </ul>
 *
 * If XY_COORD_PRECISION or Z_COORD_PRECISION is specified, COORDINATE_PRECISION
 * or SIGNIFICANT_FIGURES will be ignored if specified.
 * If COORDINATE_PRECISION is defined, SIGNIFICANT_FIGURES will be ignored if
 * specified.
 * When none are defined, the default is COORDINATE_PRECISION=15.
 *
 * This method is the same as the C++ method OGRGeometry::exportToJson().
 *
 * @param hGeometry handle to the geometry.
 * @param papszOptions a null terminated list of options.
 * @return A GeoJSON fragment or NULL in case of error.
 *
 */

char *OGR_G_ExportToJsonEx(OGRGeometryH hGeometry, char **papszOptions)
{
    VALIDATE_POINTER1(hGeometry, "OGR_G_ExportToJson", nullptr);

    OGRGeometry *poGeometry = OGRGeometry::FromHandle(hGeometry);

    const char *pszCoordPrecision =
        CSLFetchNameValueDef(papszOptions, "COORDINATE_PRECISION", "-1");

    const int nSignificantFigures =
        atoi(CSLFetchNameValueDef(papszOptions, "SIGNIFICANT_FIGURES", "-1"));

    OGRGeoJSONWriteOptions oOptions;
    oOptions.nXYCoordPrecision = atoi(CSLFetchNameValueDef(
        papszOptions, "XY_COORD_PRECISION", pszCoordPrecision));
    oOptions.nZCoordPrecision = atoi(CSLFetchNameValueDef(
        papszOptions, "Z_COORD_PRECISION", pszCoordPrecision));
    oOptions.nSignificantFigures = nSignificantFigures;
    oOptions.bAllowCurve =
        CPLTestBool(CSLFetchNameValueDef(papszOptions, "ALLOW_CURVE", "NO"));
    oOptions.bAllowMeasure =
        CPLTestBool(CSLFetchNameValueDef(papszOptions, "ALLOW_MEASURE", "NO"));

    bool bHasSwappedXY = false;
    const char *pszCoordinateOrder = CSLFetchNameValueDef(
        papszOptions, "COORDINATE_ORDER", "TRADITIONAL_GIS_ORDER");
    if (EQUAL(pszCoordinateOrder, "TRADITIONAL_GIS_ORDER"))
    {
        // If the CRS has latitude, longitude (or northing, easting) axis order,
        // and the data axis to SRS axis mapping doesn't change that order,
        // then swap X and Y values.
        const auto poSRS = poGeometry->getSpatialReference();
        if (poSRS && (poSRS->EPSGTreatsAsLatLong() ||
                      poSRS->EPSGTreatsAsNorthingEasting()))
        {
            auto anMapping =
                std::vector<int>(poSRS->GetDataAxisToSRSAxisMapping());
            anMapping.resize(2);
            if (anMapping == std::vector<int>{1, 2})
            {
                poGeometry->swapXY();
                bHasSwappedXY = true;
            }
        }
    }
    else if (EQUAL(pszCoordinateOrder, "AUTHORITY_COMPLIANT"))
    {
        const auto poSRS = poGeometry->getSpatialReference();
        if (poSRS && (poSRS->EPSGTreatsAsLatLong() ||
                      poSRS->EPSGTreatsAsNorthingEasting()))
        {
            auto anMapping =
                std::vector<int>(poSRS->GetDataAxisToSRSAxisMapping());
            anMapping.resize(2);
            if (anMapping == std::vector<int>{2, 1})
            {
                poGeometry->swapXY();
                bHasSwappedXY = true;
            }
        }
    }
    else
    {
        CPLError(CE_Failure, CPLE_NotSupported,
                 "Unsupported COORDINATE_ORDER='%s'", pszCoordinateOrder);
        return nullptr;
    }

    json_object *poObj = OGRGeoJSONWriteGeometry(poGeometry, oOptions);

    // Unswap back
    if (bHasSwappedXY)
        poGeometry->swapXY();

    if (nullptr != poObj)
    {
        char *pszJson = CPLStrdup(json_object_to_json_string(poObj));

        // Release JSON tree.
        json_object_put(poObj);

        return pszJson;
    }

    // Translation failed.
    return nullptr;
}
