/**********************************************************************
 *
 * PostGIS - Spatial Types for PostgreSQL
 * http://postgis.net
 *
 * PostGIS is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * PostGIS is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with PostGIS.  If not, see <http://www.gnu.org/licenses/>.
 *
 **********************************************************************
 *
 * Copyright (C) 2009 Paul Ramsey <pramsey@cleverelephant.ca>
 * Copyright (C) 2026 Darafei Praliaskouski <me@komzpa.net>
 *
 **********************************************************************/

#include "liblwgeom_internal.h"
#include "lwgeom_log.h"
#include "stringbuffer.h"
#include <stdbool.h>
#include <math.h>

static void lwgeom_to_wkt_sb(const LWGEOM *geom, stringbuffer_t *sb, int precision, uint8_t variant);
static void lwnurbscurve_to_wkt_sb(const LWNURBSCURVE *curve, stringbuffer_t *sb, int precision, uint8_t variant);

#define buffer_size 128

/*
* ISO format uses both Z and M qualifiers.
* Extended format only uses an M qualifier for 3DM variants, where it is not
* clear what the third dimension represents.
* SFSQL format never has more than two dimensions, so no qualifiers.
*/
static void dimension_qualifiers_to_wkt_sb(const LWGEOM *geom, stringbuffer_t *sb, uint8_t variant)
{

	if ((variant & WKT_EXTENDED) && geom->type == TINTYPE && FLAGS_GET_Z(geom->flags))
	{
		stringbuffer_append_len(sb, " Z", 2);
		if (FLAGS_GET_M(geom->flags))
			stringbuffer_append_len(sb, "M", 1);
		stringbuffer_append_len(sb, " ", 1);
		return;
	}

	/* Extended WKT: POINTM(0 0 0) */
#if 0
	if ( (variant & WKT_EXTENDED) && ! (variant & WKT_IS_CHILD) && FLAGS_GET_M(geom->flags) && (!FLAGS_GET_Z(geom->flags)) )
#else
	if ( (variant & WKT_EXTENDED) && FLAGS_GET_M(geom->flags) && (!FLAGS_GET_Z(geom->flags)) )
#endif
	{
		stringbuffer_append_len(sb, "M", 1); /* "M" */
		return;
	}

	/* ISO WKT: POINT ZM (0 0 0 0) */
	if ( (variant & WKT_ISO) && (FLAGS_NDIMS(geom->flags) > 2) )
	{
		stringbuffer_append_len(sb, " ", 1);
		if ( FLAGS_GET_Z(geom->flags) )
			stringbuffer_append_len(sb, "Z", 1);
		if ( FLAGS_GET_M(geom->flags) )
			stringbuffer_append_len(sb, "M", 1);
		stringbuffer_append_len(sb, " ", 1);
	}
}

/*
* Write an empty token out, padding with a space if
* necessary.
*/
static void empty_to_wkt_sb(stringbuffer_t *sb)
{
	if ( ! strchr(" ,(", stringbuffer_lastchar(sb)) ) /* "EMPTY" */
	{
		stringbuffer_append_len(sb, " ", 1);
	}
	stringbuffer_append_len(sb, "EMPTY", 5);
}

inline static void
coordinate_to_wkt_sb(double *coords, stringbuffer_t *sb, uint32_t dimensions, int precision)
{
	uint32_t d = 0;
	stringbuffer_append_double(sb, coords[d], precision);

	for (d = 1; d < dimensions; d++)
	{
		stringbuffer_append_len(sb, " ", 1);
		stringbuffer_append_double(sb, coords[d], precision);
	}
}

/*
* Point array is a list of coordinates. Depending on output mode,
* we may suppress some dimensions. ISO and Extended formats include
* all dimensions. Standard OGC output only includes X/Y coordinates.
*/
static void ptarray_to_wkt_sb(const POINTARRAY *ptarray, stringbuffer_t *sb, int precision, uint8_t variant)
{
	/* OGC only includes X/Y */
	uint32_t dimensions = 2;

	/* ISO and extended formats include all dimensions */
	if ( variant & ( WKT_ISO | WKT_EXTENDED ) )
		dimensions = FLAGS_NDIMS(ptarray->flags);

	stringbuffer_makeroom(sb, 2 + ((OUT_MAX_BYTES_DOUBLE + 1) * dimensions * ptarray->npoints));
	/* Opening paren? */
	if ( ! (variant & WKT_NO_PARENS) )
		stringbuffer_append_len(sb, "(", 1);

	/* Digits and commas */
	if (ptarray->npoints)
	{
		uint32_t i = 0;

		double *dbl_ptr = (double *)getPoint_internal(ptarray, i);
		coordinate_to_wkt_sb(dbl_ptr, sb, dimensions, precision);

		for (i = 1; i < ptarray->npoints; i++)
		{
			stringbuffer_append_len(sb, ",", 1);
			dbl_ptr = (double *)getPoint_internal(ptarray, i);
			coordinate_to_wkt_sb(dbl_ptr, sb, dimensions, precision);
		}
	}

	/* Closing paren? */
	if ( ! (variant & WKT_NO_PARENS) )
		stringbuffer_append_len(sb, ")", 1);
}

/*
* A four-dimensional point will have different outputs depending on variant.
*   ISO: POINT ZM (0 0 0 0)
*   Extended: POINT(0 0 0 0)
*   OGC: POINT(0 0)
* A three-dimensional m-point will have different outputs too.
*   ISO: POINT M (0 0 0)
*   Extended: POINTM(0 0 0)
*   OGC: POINT(0 0)
*/
static void lwpoint_to_wkt_sb(const LWPOINT *pt, stringbuffer_t *sb, int precision, uint8_t variant)
{
	if ( ! (variant & WKT_NO_TYPE) )
	{
		stringbuffer_append_len(sb, "POINT", 5); /* "POINT" */
		dimension_qualifiers_to_wkt_sb((LWGEOM*)pt, sb, variant);
	}

	if ( lwpoint_is_empty(pt) )
	{
		empty_to_wkt_sb(sb);
		return;
	}

	ptarray_to_wkt_sb(pt->point, sb, precision, variant);
}

/*
* LINESTRING(0 0 0, 1 1 1)
*/
static void lwline_to_wkt_sb(const LWLINE *line, stringbuffer_t *sb, int precision, uint8_t variant)
{
	if ( ! (variant & WKT_NO_TYPE) )
	{
		stringbuffer_append_len(sb, "LINESTRING", 10); /* "LINESTRING" */
		dimension_qualifiers_to_wkt_sb((LWGEOM*)line, sb, variant);
	}
	if ( lwline_is_empty(line) )
	{
		empty_to_wkt_sb(sb);
		return;
	}

    ptarray_to_wkt_sb(line->points, sb, precision, variant);
}

/*
* POLYGON(0 0 1, 1 0 1, 1 1 1, 0 1 1, 0 0 1)
*/
static void lwpoly_to_wkt_sb(const LWPOLY *poly, stringbuffer_t *sb, int precision, uint8_t variant)
{
	uint32_t i = 0;
	if ( ! (variant & WKT_NO_TYPE) )
	{
		stringbuffer_append_len(sb, "POLYGON", 7); /* "POLYGON" */
		dimension_qualifiers_to_wkt_sb((LWGEOM*)poly, sb, variant);
	}
	if ( lwpoly_is_empty(poly) )
	{
		empty_to_wkt_sb(sb);
		return;
	}

	stringbuffer_append_len(sb, "(", 1);
	for ( i = 0; i < poly->nrings; i++ )
	{
		if ( i > 0 )
			stringbuffer_append_len(sb, ",", 1);
		ptarray_to_wkt_sb(poly->rings[i], sb, precision, variant);
	}
	stringbuffer_append_len(sb, ")", 1);
}

/*
* CIRCULARSTRING
*/
static void lwcircstring_to_wkt_sb(const LWCIRCSTRING *circ, stringbuffer_t *sb, int precision, uint8_t variant)
{
	if ( ! (variant & WKT_NO_TYPE) )
	{
		stringbuffer_append_len(sb, "CIRCULARSTRING", 14); /* "CIRCULARSTRING" */
		dimension_qualifiers_to_wkt_sb((LWGEOM*)circ, sb, variant);
	}
	if ( lwcircstring_is_empty(circ) )
	{
		empty_to_wkt_sb(sb);
		return;
	}
	ptarray_to_wkt_sb(circ->points, sb, precision, variant);
}


/*
* Multi-points, in non-ISO format, do not wrap their sub-members in parens, unlike other multi-geometries.
*   MULTPOINT(0 0, 1 1) instead of MULTIPOINT((0 0),(1 1))
* Strictly speaking, the SFA spec also mandates use of parens in sub-members, but
* use the old non-parens interpretation for WKT_SFSQL
*/
static void lwmpoint_to_wkt_sb(const LWMPOINT *mpoint, stringbuffer_t *sb, int precision, uint8_t variant)
{
	uint32_t i = 0;
	if ( ! (variant & WKT_NO_TYPE) )
	{
		stringbuffer_append_len(sb, "MULTIPOINT", 10); /* "MULTIPOINT" */
		dimension_qualifiers_to_wkt_sb((LWGEOM*)mpoint, sb, variant);
	}
	if ( mpoint->ngeoms < 1 )
	{
		empty_to_wkt_sb(sb);
		return;
	}
	stringbuffer_append_len(sb, "(", 1);
	variant = variant | WKT_IS_CHILD | WKT_NO_TYPE; /* Inform the sub-geometries they are children */
	if ( !(variant & WKT_ISO) )
		variant = variant | WKT_NO_PARENS;
	for ( i = 0; i < mpoint->ngeoms; i++ )
	{
		if ( i > 0 )
			stringbuffer_append_len(sb, ",", 1);
		/* We don't want type strings or parens on our subgeoms */
		lwpoint_to_wkt_sb(mpoint->geoms[i], sb, precision, variant);
	}
	stringbuffer_append_len(sb, ")", 1);
}

/*
* MULTILINESTRING
*/
static void lwmline_to_wkt_sb(const LWMLINE *mline, stringbuffer_t *sb, int precision, uint8_t variant)
{
	uint32_t i = 0;

	if ( ! (variant & WKT_NO_TYPE) )
	{
		stringbuffer_append_len(sb, "MULTILINESTRING", 15); /* "MULTILINESTRING" */
		dimension_qualifiers_to_wkt_sb((LWGEOM*)mline, sb, variant);
	}
	if ( mline->ngeoms < 1 )
	{
		empty_to_wkt_sb(sb);
		return;
	}

	stringbuffer_append_len(sb, "(", 1);
	variant = variant | WKT_IS_CHILD; /* Inform the sub-geometries they are childre */
	for ( i = 0; i < mline->ngeoms; i++ )
	{
		if ( i > 0 )
			stringbuffer_append_len(sb, ",", 1);
		/* We don't want type strings on our subgeoms */
		lwline_to_wkt_sb(mline->geoms[i], sb, precision, variant | WKT_NO_TYPE );
	}
	stringbuffer_append_len(sb, ")", 1);
}

/*
* MULTIPOLYGON
*/
static void lwmpoly_to_wkt_sb(const LWMPOLY *mpoly, stringbuffer_t *sb, int precision, uint8_t variant)
{
	uint32_t i = 0;

	if ( ! (variant & WKT_NO_TYPE) )
	{
		stringbuffer_append_len(sb, "MULTIPOLYGON", 12); /* "MULTIPOLYGON" */
		dimension_qualifiers_to_wkt_sb((LWGEOM*)mpoly, sb, variant);
	}
	if ( mpoly->ngeoms < 1 )
	{
		empty_to_wkt_sb(sb);
		return;
	}

	stringbuffer_append_len(sb, "(", 1);
	variant = variant | WKT_IS_CHILD; /* Inform the sub-geometries they are childre */
	for ( i = 0; i < mpoly->ngeoms; i++ )
	{
		if ( i > 0 )
			stringbuffer_append_len(sb, ",", 1);
		/* We don't want type strings on our subgeoms */
		lwpoly_to_wkt_sb(mpoly->geoms[i], sb, precision, variant | WKT_NO_TYPE );
	}
	stringbuffer_append_len(sb, ")", 1);
}

/*
* Compound curves provide type information for their curved sub-geometries
* but not their linestring sub-geometries.
*   COMPOUNDCURVE((0 0, 1 1), CURVESTRING(1 1, 2 2, 3 3))
*/
static void lwcompound_to_wkt_sb(const LWCOMPOUND *comp, stringbuffer_t *sb, int precision, uint8_t variant)
{
	uint32_t i = 0;

	if ( ! (variant & WKT_NO_TYPE) )
	{
		stringbuffer_append_len(sb, "COMPOUNDCURVE", 13); /* "COMPOUNDCURVE" */
		dimension_qualifiers_to_wkt_sb((LWGEOM*)comp, sb, variant);
	}
	if ( comp->ngeoms < 1 )
	{
		empty_to_wkt_sb(sb);
		return;
	}

	stringbuffer_append_len(sb, "(", 1);
	variant = variant | WKT_IS_CHILD; /* Inform the sub-geometries they are childre */
	for ( i = 0; i < comp->ngeoms; i++ )
	{
		int type = comp->geoms[i]->type;
		if ( i > 0 )
			stringbuffer_append_len(sb, ",", 1);
		/* Linestring subgeoms don't get type identifiers */
		if ( type == LINETYPE )
		{
			lwline_to_wkt_sb((LWLINE*)comp->geoms[i], sb, precision, variant | WKT_NO_TYPE );
		}
		/* But circstring subgeoms *do* get type identifiers */
		else if ( type == CIRCSTRINGTYPE )
		{
			lwcircstring_to_wkt_sb((LWCIRCSTRING*)comp->geoms[i], sb, precision, variant );
		}
		else if ( type == NURBSCURVETYPE )
		{
			lwnurbscurve_to_wkt_sb((LWNURBSCURVE*)comp->geoms[i], sb, precision, variant );
		}
		else
		{
			lwerror("lwcompound_to_wkt_sb: Unknown type received %d - %s", type, lwtype_name(type));
		}
	}
	stringbuffer_append_len(sb, ")", 1);
}

/*
* Curve polygons provide type information for their curved rings
* but not their linestring rings.
*   CURVEPOLYGON((0 0, 1 1, 0 1, 0 0), CURVESTRING(0 0, 1 1, 0 1, 0.5 1, 0 0))
*/
static void lwcurvepoly_to_wkt_sb(const LWCURVEPOLY *cpoly, stringbuffer_t *sb, int precision, uint8_t variant)
{
	uint32_t i = 0;

	if ( ! (variant & WKT_NO_TYPE) )
	{
		stringbuffer_append_len(sb, "CURVEPOLYGON", 12); /* "CURVEPOLYGON" */
		dimension_qualifiers_to_wkt_sb((LWGEOM*)cpoly, sb, variant);
	}
	if ( cpoly->nrings < 1 )
	{
		empty_to_wkt_sb(sb);
		return;
	}
	stringbuffer_append_len(sb, "(", 1);
	variant = variant | WKT_IS_CHILD; /* Inform the sub-geometries they are childre */
	for ( i = 0; i < cpoly->nrings; i++ )
	{
		int type = cpoly->rings[i]->type;
		if ( i > 0 )
			stringbuffer_append_len(sb, ",", 1);
		switch (type)
		{
		case LINETYPE:
			/* Linestring subgeoms don't get type identifiers */
			lwline_to_wkt_sb((LWLINE*)cpoly->rings[i], sb, precision, variant | WKT_NO_TYPE );
			break;
		case CIRCSTRINGTYPE:
			/* But circstring subgeoms *do* get type identifiers */
			lwcircstring_to_wkt_sb((LWCIRCSTRING*)cpoly->rings[i], sb, precision, variant );
			break;
		case COMPOUNDTYPE:
			/* And compoundcurve subgeoms *do* get type identifiers */
			lwcompound_to_wkt_sb((LWCOMPOUND*)cpoly->rings[i], sb, precision, variant );
			break;
		case NURBSCURVETYPE:
			lwnurbscurve_to_wkt_sb((LWNURBSCURVE*)cpoly->rings[i], sb, precision, variant );
			break;
		default:
			lwerror("lwcurvepoly_to_wkt_sb: Unknown type received %d - %s", type, lwtype_name(type));
		}
	}
	stringbuffer_append_len(sb, ")", 1);
}


/*
* Multi-curves provide type information for their curved sub-geometries
* but not their linear sub-geometries.
*   MULTICURVE((0 0, 1 1), CURVESTRING(0 0, 1 1, 2 2))
*/
static void lwmcurve_to_wkt_sb(const LWMCURVE *mcurv, stringbuffer_t *sb, int precision, uint8_t variant)
{
	uint32_t i = 0;

	if ( ! (variant & WKT_NO_TYPE) )
	{
		stringbuffer_append_len(sb, "MULTICURVE", 10); /* "MULTICURVE" */
		dimension_qualifiers_to_wkt_sb((LWGEOM*)mcurv, sb, variant);
	}
	if ( mcurv->ngeoms < 1 )
	{
		empty_to_wkt_sb(sb);
		return;
	}
	stringbuffer_append_len(sb, "(", 1);
	variant = variant | WKT_IS_CHILD; /* Inform the sub-geometries they are childre */
	for ( i = 0; i < mcurv->ngeoms; i++ )
	{
		int type = mcurv->geoms[i]->type;
		if ( i > 0 )
			stringbuffer_append_len(sb, ",", 1);
		switch (type)
		{
		case LINETYPE:
			/* Linestring subgeoms don't get type identifiers */
			lwline_to_wkt_sb((LWLINE*)mcurv->geoms[i], sb, precision, variant | WKT_NO_TYPE );
			break;
		case CIRCSTRINGTYPE:
			/* But circstring subgeoms *do* get type identifiers */
			lwcircstring_to_wkt_sb((LWCIRCSTRING*)mcurv->geoms[i], sb, precision, variant );
			break;
		case COMPOUNDTYPE:
			/* And compoundcurve subgeoms *do* get type identifiers */
			lwcompound_to_wkt_sb((LWCOMPOUND*)mcurv->geoms[i], sb, precision, variant );
			break;
		case NURBSCURVETYPE:
			lwnurbscurve_to_wkt_sb((LWNURBSCURVE*)mcurv->geoms[i], sb, precision, variant );
			break;
		default:
			lwerror("lwmcurve_to_wkt_sb: Unknown type received %d - %s", type, lwtype_name(type));
		}
	}
	stringbuffer_append_len(sb, ")", 1);
}


/*
* Multi-surfaces provide type information for their curved sub-geometries
* but not their linear sub-geometries.
*   MULTISURFACE(((0 0, 1 1, 1 0, 0 0)), CURVEPOLYGON(CURVESTRING(0 0, 1 1, 2 2, 0 1, 0 0)))
*/
static void lwmsurface_to_wkt_sb(const LWMSURFACE *msurf, stringbuffer_t *sb, int precision, uint8_t variant)
{
	uint32_t i = 0;

	if ( ! (variant & WKT_NO_TYPE) )
	{
		stringbuffer_append_len(sb, "MULTISURFACE", 12); /* "MULTISURFACE" */
		dimension_qualifiers_to_wkt_sb((LWGEOM*)msurf, sb, variant);
	}
	if ( msurf->ngeoms < 1 )
	{
		empty_to_wkt_sb(sb);
		return;
	}
	stringbuffer_append_len(sb, "(", 1);
	variant = variant | WKT_IS_CHILD; /* Inform the sub-geometries they are childre */
	for ( i = 0; i < msurf->ngeoms; i++ )
	{
		int type = msurf->geoms[i]->type;
		if ( i > 0 )
			stringbuffer_append_len(sb, ",", 1);
		switch (type)
		{
		case POLYGONTYPE:
			/* Linestring subgeoms don't get type identifiers */
			lwpoly_to_wkt_sb((LWPOLY*)msurf->geoms[i], sb, precision, variant | WKT_NO_TYPE );
			break;
		case CURVEPOLYTYPE:
			/* But circstring subgeoms *do* get type identifiers */
			lwcurvepoly_to_wkt_sb((LWCURVEPOLY*)msurf->geoms[i], sb, precision, variant);
			break;
		default:
			lwerror("lwmsurface_to_wkt_sb: Unknown type received %d - %s", type, lwtype_name(type));
		}
	}
	stringbuffer_append_len(sb, ")", 1);
}

/*
* Geometry collections provide type information for all their curved sub-geometries
* but not their linear sub-geometries.
*   GEOMETRYCOLLECTION(POLYGON((0 0, 1 1, 1 0, 0 0)), CURVEPOLYGON(CURVESTRING(0 0, 1 1, 2 2, 0 1, 0 0)))
*/
static void lwcollection_to_wkt_sb(const LWCOLLECTION *collection, stringbuffer_t *sb, int precision, uint8_t variant)
{
	uint32_t i = 0;

	if ( ! (variant & WKT_NO_TYPE) )
	{
		stringbuffer_append_len(sb, "GEOMETRYCOLLECTION", 18); /* "GEOMETRYCOLLECTION" */
		dimension_qualifiers_to_wkt_sb((LWGEOM*)collection, sb, variant);
	}
	if ( collection->ngeoms < 1 )
	{
		empty_to_wkt_sb(sb);
		return;
	}
	stringbuffer_append_len(sb, "(", 1);
	variant = variant | WKT_IS_CHILD; /* Inform the sub-geometries they are children */
	for ( i = 0; i < collection->ngeoms; i++ )
	{
		if ( i > 0 )
			stringbuffer_append_len(sb, ",", 1);
		lwgeom_to_wkt_sb((LWGEOM*)collection->geoms[i], sb, precision, variant );
	}
	stringbuffer_append_len(sb, ")", 1);
}

/*
* TRIANGLE
*/
static void lwtriangle_to_wkt_sb(const LWTRIANGLE *tri, stringbuffer_t *sb, int precision, uint8_t variant)
{
	if ( ! (variant & WKT_NO_TYPE) )
	{
		stringbuffer_append_len(sb, "TRIANGLE", 8); /* "TRIANGLE" */
		dimension_qualifiers_to_wkt_sb((LWGEOM*)tri, sb, variant);
	}
	if ( lwtriangle_is_empty(tri) )
	{
		empty_to_wkt_sb(sb);
		return;
	}

	stringbuffer_append_len(sb, "(", 1); /* Triangles have extraneous brackets */
	ptarray_to_wkt_sb(tri->points, sb, precision, variant);
	stringbuffer_append_len(sb, ")", 1);
}

/*
* TIN
*/
static void lwtin_to_wkt_sb(const LWTIN *tin, stringbuffer_t *sb, int precision, uint8_t variant)
{
	uint32_t i = 0;

	if ( ! (variant & WKT_NO_TYPE) )
	{
		stringbuffer_append_len(sb, "TIN", 3); /* "TIN" */
		dimension_qualifiers_to_wkt_sb((LWGEOM*)tin, sb, variant);
	}
	if ( tin->ngeoms < 1 )
	{
		empty_to_wkt_sb(sb);
		return;
	}

	stringbuffer_append_len(sb, "(", 1);
	for ( i = 0; i < tin->ngeoms; i++ )
	{
		if ( i > 0 )
			stringbuffer_append_len(sb, ",", 1);
		/* We don't want type strings on our subgeoms */
		lwtriangle_to_wkt_sb(tin->geoms[i], sb, precision, variant | WKT_NO_TYPE );
	}
	stringbuffer_append_len(sb, ")", 1);
}

/**
 * Emit a POLYHEDRALSURFACE WKT representation for the given LWPSURFACE.
 *
 * Writes the WKT form (e.g. `POLYHEDRALSURFACE ...`) into the provided string buffer,
 * including dimension qualifiers unless suppressed by the variant flags. If the surface
 * contains no polygons, emits the `EMPTY` token. Sub-geometries are emitted as child
 * geometries (their type tokens are suppressed).
 *
 * @param psurf Polyhedral surface to convert to WKT.
 * @param sb String buffer to append the generated WKT to.
 * @param precision Number of decimal digits to use when formatting coordinates.
 * @param variant Bitmask of WKT emission options (e.g. WKT_NO_TYPE, WKT_IS_CHILD, WKT_EXTENDED).
 */
static void lwpsurface_to_wkt_sb(const LWPSURFACE *psurf, stringbuffer_t *sb, int precision, uint8_t variant)
{
	uint32_t i = 0;

	if ( ! (variant & WKT_NO_TYPE) )
	{
		stringbuffer_append_len(sb, "POLYHEDRALSURFACE", 17); /* "POLYHEDRALSURFACE" */
		dimension_qualifiers_to_wkt_sb((LWGEOM*)psurf, sb, variant);
	}
	if ( psurf->ngeoms < 1 )
	{
		empty_to_wkt_sb(sb);
		return;
	}

	variant = variant | WKT_IS_CHILD; /* Inform the sub-geometries they are childre */

	stringbuffer_append_len(sb, "(", 1);
	for ( i = 0; i < psurf->ngeoms; i++ )
	{
		if ( i > 0 )
			stringbuffer_append_len(sb, ",", 1);
		/* We don't want type strings on our subgeoms */
		lwpoly_to_wkt_sb(psurf->geoms[i], sb, precision, variant | WKT_NO_TYPE );
	}
	stringbuffer_append_len(sb, ")", 1);
}

/**
 * Emit a NURBSCURVE as WKT into the provided stringbuffer.
 *
 * Produces the WKT representation for the given NURBSCURVE and appends it to
 * sb. If the variant does not include WKT_NO_TYPE, the leading type token
 * "NURBSCURVE" (with appropriate dimension qualifiers) is written. If the
 * curve has no control points the token "EMPTY" is emitted.
 *
 * The WKT payload follows the ISO form: DEGREE, CONTROLPOINTS containing
 * NURBSPOINT/WEIGHTEDPOINT entries, and KNOTS containing collapsed knot
 * multiplicities. Default uniform weights and knots are emitted to preserve
 * round-trip consistency.
 *
 * Numeric values (coordinates, weights, knots) are formatted using the given
 * precision (passed to printf-style formatting).
 *
 * @param curve NURBSCURVE to format.
 * @param sb Destination stringbuffer; WKT text is appended to it.
 * @param precision Number of significant digits used when formatting numeric values.
 * @param variant Bitmask of WKT emission flags (e.g., WKT_NO_TYPE to suppress the type token).
 */
static void lwnurbscurve_to_wkt_sb(const LWNURBSCURVE *curve, stringbuffer_t *sb, int precision, uint8_t variant)
{
	uint32_t dimensions;
	uint8_t output_variant = variant | WKT_ISO;
	uint32_t nknots = 0;
	double *knots = NULL;

	if (!(variant & WKT_NO_TYPE))
	{
		stringbuffer_append_len(sb, "NURBSCURVE", 10);
		dimension_qualifiers_to_wkt_sb((LWGEOM*)curve, sb, output_variant);
	}

	if (!curve->points || curve->points->npoints == 0)
	{
		empty_to_wkt_sb(sb);
		return;
	}

	stringbuffer_append_len(sb, "(", 1);
	stringbuffer_aprintf(sb, "DEGREE %d,CONTROLPOINTS", curve->degree);
	dimension_qualifiers_to_wkt_sb((LWGEOM*)curve, sb, output_variant);
	stringbuffer_append_len(sb, "(", 1);

	dimensions = (output_variant & (WKT_ISO | WKT_EXTENDED)) ? FLAGS_NDIMS(curve->points->flags) : 2;
	for (uint32_t i = 0; i < curve->points->npoints; i++)
	{
		double *coords = (double *)getPoint_internal(curve->points, i);
		double weight = (curve->weights && i < curve->nweights) ? curve->weights[i] : 1.0;

		if (i > 0)
			stringbuffer_append_len(sb, ",", 1);

		stringbuffer_append_len(sb, "NURBSPOINT(WEIGHTEDPOINT", 24);
		dimension_qualifiers_to_wkt_sb((LWGEOM*)curve, sb, output_variant);
		stringbuffer_append_len(sb, "(", 1);
		coordinate_to_wkt_sb(coords, sb, dimensions, precision);
		stringbuffer_append_len(sb, "),WEIGHT ", strlen("),WEIGHT "));
		stringbuffer_append_double(sb, weight, precision);
		stringbuffer_append_len(sb, ")", 1);
	}
	stringbuffer_append_len(sb, ")", 1);

	knots = lwnurbscurve_get_or_generate_knots(curve, &nknots);
	if (knots && nknots > 0)
	{
		uint32_t knot_count = 0;
		stringbuffer_append_len(sb, ",KNOTS (", 8);
		for (uint32_t i = 0; i < nknots; i += knot_count)
		{
			knot_count = 1;
			while (i + knot_count < nknots && fabs(knots[i + knot_count] - knots[i]) <= FP_TOLERANCE)
				knot_count++;

			if (i > 0)
				stringbuffer_append_len(sb, ",", 1);
			stringbuffer_append_len(sb, "KNOT(", 5);
			stringbuffer_append_double(sb, knots[i], precision);
			stringbuffer_append_len(sb, ",", 1);
			stringbuffer_aprintf(sb, "%d", knot_count);
			stringbuffer_append_len(sb, ")", 1);
		}
		stringbuffer_append_len(sb, ")", 1);
		lwfree(knots);
	}

	stringbuffer_append_len(sb, ")", 1);
}

/**
 * Dispatches a geometry to the appropriate WKT emitter and appends result to a string buffer.
 *
 * Given a concrete LWGEOM subclass, selects the matching type-specific
 * writer (POINT, LINESTRING, POLYGON, MULTI*, CURVE, etc.) and emits its
 * Well-Known Text representation into the provided stringbuffer.
 *
 * @param geom Input geometry (may not be NULL).
 * @param sb Destination stringbuffer to which WKT text is appended.
 * @param precision Number of decimal digits to use when formatting coordinates.
 * @param variant Bitmask selecting WKT variant/formatting flags (e.g., ISO/Extended/SFSQL and child/parenthesis behaviors).
 *
 * Note: Unsupported geometry types are reported via lwerror; this function itself does not return an error code. It has the side effect of modifying the supplied stringbuffer.
 */
static void lwgeom_to_wkt_sb(const LWGEOM *geom, stringbuffer_t *sb, int precision, uint8_t variant)
{
	LWDEBUGF(4, "lwgeom_to_wkt_sb: type %s, hasz %d, hasm %d",
		lwtype_name(geom->type),
		FLAGS_GET_Z(geom->flags)?1:0, FLAGS_GET_M(geom->flags)?1:0);

	switch (geom->type)
	{
	case POINTTYPE:
		lwpoint_to_wkt_sb((LWPOINT*)geom, sb, precision, variant);
		break;
	case LINETYPE:
		lwline_to_wkt_sb((LWLINE*)geom, sb, precision, variant);
		break;
	case POLYGONTYPE:
		lwpoly_to_wkt_sb((LWPOLY*)geom, sb, precision, variant);
		break;
	case MULTIPOINTTYPE:
		lwmpoint_to_wkt_sb((LWMPOINT*)geom, sb, precision, variant);
		break;
	case MULTILINETYPE:
		lwmline_to_wkt_sb((LWMLINE*)geom, sb, precision, variant);
		break;
	case MULTIPOLYGONTYPE:
		lwmpoly_to_wkt_sb((LWMPOLY*)geom, sb, precision, variant);
		break;
	case COLLECTIONTYPE:
		lwcollection_to_wkt_sb((LWCOLLECTION*)geom, sb, precision, variant);
		break;
	case CIRCSTRINGTYPE:
		lwcircstring_to_wkt_sb((LWCIRCSTRING*)geom, sb, precision, variant);
		break;
	case COMPOUNDTYPE:
		lwcompound_to_wkt_sb((LWCOMPOUND*)geom, sb, precision, variant);
		break;
	case CURVEPOLYTYPE:
		lwcurvepoly_to_wkt_sb((LWCURVEPOLY*)geom, sb, precision, variant);
		break;
	case MULTICURVETYPE:
		lwmcurve_to_wkt_sb((LWMCURVE*)geom, sb, precision, variant);
		break;
	case MULTISURFACETYPE:
		lwmsurface_to_wkt_sb((LWMSURFACE*)geom, sb, precision, variant);
		break;
	case TRIANGLETYPE:
		lwtriangle_to_wkt_sb((LWTRIANGLE*)geom, sb, precision, variant);
		break;
	case TINTYPE:
		lwtin_to_wkt_sb((LWTIN*)geom, sb, precision, variant);
		break;
	case POLYHEDRALSURFACETYPE:
		lwpsurface_to_wkt_sb((LWPSURFACE*)geom, sb, precision, variant);
		break;
	case NURBSCURVETYPE:
		lwnurbscurve_to_wkt_sb((LWNURBSCURVE*)geom, sb, precision, variant);
		break;
	default:
		lwerror("lwgeom_to_wkt_sb: Type %d - %s unsupported.",
		        geom->type, lwtype_name(geom->type));
	}
}

static stringbuffer_t *
lwgeom_to_wkt_internal(const LWGEOM *geom, uint8_t variant, int precision)
{
	stringbuffer_t *sb;
	if ( geom == NULL )
		return NULL;
	sb = stringbuffer_create();
	/* Extended mode starts with an "SRID=" section for geoms that have one */
	if ( (variant & WKT_EXTENDED) && lwgeom_has_srid(geom) )
	{
		stringbuffer_aprintf(sb, "SRID=%d;", geom->srid);
	}
	lwgeom_to_wkt_sb(geom, sb, precision, variant);
	if ( stringbuffer_getstring(sb) == NULL )
	{
		lwerror("Uh oh");
		return NULL;
	}
	return sb;
}

/**
 * WKT emitter function. Allocates a new *char and fills it with the WKT
 * representation. If size_out is not NULL, it will be set to the size of the
 * allocated *char.
 *
 * @param variant Bitmasked value, accepts one of WKT_ISO, WKT_SFSQL,
 * WKT_EXTENDED.
 * @param precision Maximal number of digits after comma in the output doubles.
 * @param size_out If supplied, will return the size of the returned string,
 * including the null terminator.
 */
char *
lwgeom_to_wkt(const LWGEOM *geom, uint8_t variant, int precision, size_t *size_out)
{
	stringbuffer_t *sb = lwgeom_to_wkt_internal(geom, variant, precision);
	if (!sb)
		return NULL;
	char *str = stringbuffer_getstringcopy(sb);
	if ( size_out )
		*size_out = stringbuffer_getlength(sb) + 1;
	stringbuffer_destroy(sb);
	return str;
}

lwvarlena_t *
lwgeom_to_wkt_varlena(const LWGEOM *geom, uint8_t variant, int precision)
{
	stringbuffer_t *sb = lwgeom_to_wkt_internal(geom, variant, precision);
	if (!sb)
		return NULL;
	lwvarlena_t *output = stringbuffer_getvarlenacopy(sb);
	stringbuffer_destroy(sb);
	return output;
}
