/**********************************************************************
 *
 * 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 2026 Darafei Praliaskouski <me@komzpa.net>
 * Copyright 2009-2014 Sandro Santilli <strk@kbt.io>
 * Copyright 2008 Paul Ramsey <pramsey@cleverelephant.ca>
 * Copyright 2001-2003 Refractions Research Inc.
 * Copyright 2026 Darafei Praliaskouski <me@komzpa.net>
 *
 **********************************************************************/

#include "../postgis_config.h"

/* PostgreSQL */
#include "postgres.h"
#include "funcapi.h"
#include "utils/array.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
#include "utils/numeric.h"
#include "access/htup_details.h"

/* PostGIS */
#include "lwgeom_geos.h"
#include "liblwgeom.h"
#include "liblwgeom_internal.h"
#include "lwgeom_itree.h"
#include "lwgeom_geos_prepared.h"
#include "lwgeom_accum.h"


/* Prototypes for SQL-bound functions */
Datum LWGEOM_dwithin(PG_FUNCTION_ARGS);
Datum relate_full(PG_FUNCTION_ARGS);
Datum relate_pattern(PG_FUNCTION_ARGS);
Datum disjoint(PG_FUNCTION_ARGS);
Datum touches(PG_FUNCTION_ARGS);
Datum ST_Intersects(PG_FUNCTION_ARGS);
Datum crosses(PG_FUNCTION_ARGS);
Datum contains(PG_FUNCTION_ARGS);
Datum within(PG_FUNCTION_ARGS);
Datum containsproperly(PG_FUNCTION_ARGS);
Datum covers(PG_FUNCTION_ARGS);
Datum overlaps(PG_FUNCTION_ARGS);
Datum coveredby(PG_FUNCTION_ARGS);
Datum ST_Equals(PG_FUNCTION_ARGS);


/*
 * Utility to quickly check for polygonal geometries
 */
static inline uint8_t
is_poly(const GSERIALIZED *g)
{
    int type = gserialized_get_type(g);
    return type == POLYGONTYPE || type == MULTIPOLYGONTYPE;
}

/*
 * Cheap type gate for geometries that may become point-like.
 */
static inline uint8_t
is_point_or_collection(const GSERIALIZED *g)
{
	int type = gserialized_get_type(g);
	return type == POINTTYPE || type == MULTIPOINTTYPE || type == COLLECTIONTYPE;
}

/* Avoid homogenizing mixed collections just to discover that PIP cannot use them. */
static uint8_t
lwgeom_is_pointlike(const LWGEOM *lwgeom)
{
	const LWCOLLECTION *lwcol;
	uint32_t i;

	if (lwgeom->type == POINTTYPE || lwgeom->type == MULTIPOINTTYPE)
		return LW_TRUE;

	if (lwgeom->type != COLLECTIONTYPE)
		return LW_FALSE;

	lwcol = (const LWCOLLECTION *)lwgeom;
	if (lwcol->ngeoms == 0)
		return LW_FALSE;

	for (i = 0; i < lwcol->ngeoms; i++)
	{
		if (!lwgeom_is_pointlike(lwcol->geoms[i]))
			return LW_FALSE;
	}

	return LW_TRUE;
}

/*
 * Return an owned point/multipoint LWGEOM for direct point inputs or for
 * GeometryCollections whose children are all points.
 */
static LWGEOM *
pointlike_lwgeom_from_gserialized(const GSERIALIZED *g)
{
	int type = gserialized_get_type(g);
	LWGEOM *lwgeom;
	LWGEOM *hgeom;

	if (type == POINTTYPE || type == MULTIPOINTTYPE)
		return lwgeom_from_gserialized(g);

	if (type != COLLECTIONTYPE)
		return NULL;

	lwgeom = lwgeom_from_gserialized(g);
	if (!lwgeom_is_pointlike(lwgeom))
	{
		lwgeom_free(lwgeom);
		return NULL;
	}

	hgeom = lwgeom_homogenize(lwgeom);
	lwgeom_free(lwgeom);

	if (!hgeom)
		return NULL;

	if (hgeom->type == POINTTYPE || hgeom->type == MULTIPOINTTYPE)
		return hgeom;

	lwgeom_free(hgeom);
	return NULL;
}

typedef bool (*itree_pip_predicate)(const IntervalTree *itree, const LWGEOM *lwpoints);

static bool
try_itree_pointlike_pip(FunctionCallInfo fcinfo,
			const GSERIALIZED *gpoints,
			SHARED_GSERIALIZED *shared_gpoly,
			itree_pip_predicate predicate,
			bool *result)
{
	LWGEOM *lwpt = pointlike_lwgeom_from_gserialized(gpoints);
	IntervalTree *itree;

	if (!lwpt)
		return false;

	itree = GetIntervalTree(fcinfo, shared_gpoly);
	*result = predicate(itree, lwpt);
	lwgeom_free(lwpt);
	return true;
}

PG_FUNCTION_INFO_V1(ST_Intersects);
Datum ST_Intersects(PG_FUNCTION_ARGS)
{
	SHARED_GSERIALIZED *shared_geom1 = ToastCacheGetGeometry(fcinfo, 0);
	SHARED_GSERIALIZED *shared_geom2 = ToastCacheGetGeometry(fcinfo, 1);
	const GSERIALIZED *geom1 = shared_gserialized_get(shared_geom1);
	const GSERIALIZED *geom2 = shared_gserialized_get(shared_geom2);
	int8_t result;
	GBOX box1, box2;
	PrepGeomCache *prep_cache;
	bool pip_result;

	gserialized_error_if_srid_mismatch(geom1, geom2, __func__);

	/* A.Intersects(Empty) == FALSE */
	if ( gserialized_is_empty(geom1) || gserialized_is_empty(geom2) )
		PG_RETURN_BOOL(false);

	/*
	 * Short-circuit 1: if geom2 bounding box does not overlap
	 * geom1 bounding box we can return FALSE.
	 */
	if ( gserialized_get_gbox_p(geom1, &box1) &&
	     gserialized_get_gbox_p(geom2, &box2) )
	{
		if ( gbox_overlaps_2d(&box1, &box2) == LW_FALSE )
			PG_RETURN_BOOL(false);
	}

	/*
	 * Short-circuit 2: if one geometry is polygonal and the other is a
	 * point or point-only collection, use the IntervalTree PIP fast path.
	 */
	if (is_point_or_collection(geom1) && is_poly(geom2) &&
	    try_itree_pointlike_pip(fcinfo, geom1, shared_geom2, itree_pip_intersects, &pip_result))
	{
		PG_RETURN_BOOL(pip_result);
	}
	else if (is_point_or_collection(geom2) && is_poly(geom1) &&
		 try_itree_pointlike_pip(fcinfo, geom2, shared_geom1, itree_pip_intersects, &pip_result))
	{
		PG_RETURN_BOOL(pip_result);
	}

	initGEOS(lwpgnotice, lwgeom_geos_error);

	prep_cache = GetPrepGeomCache(fcinfo, shared_geom1, shared_geom2);
	if ( prep_cache && prep_cache->prepared_geom )
	{
		GEOSGeometry *g = prep_cache->gcache.argnum == 1
			? POSTGIS2GEOS(geom2)
			: POSTGIS2GEOS(geom1);
		if (!g) HANDLE_GEOS_ERROR("Geometry could not be converted to GEOS");
		result = GEOSPreparedIntersects(prep_cache->prepared_geom, g);
		GEOSGeom_destroy(g);
	}
	else
	{
		GEOSGeometry *g1, *g2;
		g1 = POSTGIS2GEOS(geom1);
		if (!g1) HANDLE_GEOS_ERROR("First argument geometry could not be converted to GEOS");
		g2 = POSTGIS2GEOS(geom2);
		if (!g2)
		{
			GEOSGeom_destroy(g1);
			HANDLE_GEOS_ERROR("Second argument geometry could not be converted to GEOS");
		}
		result = GEOSIntersects(g1, g2);
		GEOSGeom_destroy(g1);
		GEOSGeom_destroy(g2);
	}

	if (result == 2) HANDLE_GEOS_ERROR("GEOSIntersects");

	PG_RETURN_BOOL(result);
}


PG_FUNCTION_INFO_V1(ST_Equals);
Datum ST_Equals(PG_FUNCTION_ARGS)
{
	SHARED_GSERIALIZED *shared_geom1 = ToastCacheGetGeometry(fcinfo, 0);
	SHARED_GSERIALIZED *shared_geom2 = ToastCacheGetGeometry(fcinfo, 1);
	const GSERIALIZED *geom1 = shared_gserialized_get(shared_geom1);
	const GSERIALIZED *geom2 = shared_gserialized_get(shared_geom2);
	int8_t result;
	GBOX box1, box2;
	GEOSGeometry *g1, *g2;

	gserialized_error_if_srid_mismatch(geom1, geom2, __func__);

	/* Empty == Empty */
	if ( gserialized_is_empty(geom1) && gserialized_is_empty(geom2) )
		PG_RETURN_BOOL(true);

	/*
	 * Short-circuit: If geom1 and geom2 do not have the same bounding box
	 * we can return FALSE.
	 */
	if ( gserialized_get_gbox_p(geom1, &box1) &&
	     gserialized_get_gbox_p(geom2, &box2) )
	{
		if ( gbox_same_2d_float(&box1, &box2) == LW_FALSE )
		{
			PG_RETURN_BOOL(false);
		}
	}

	/*
	 * Short-circuit: if geom1 and geom2 are binary-equivalent, we can return
	 * TRUE.  This is much faster than doing the comparison using GEOS.
	 */
	if (VARSIZE(geom1) == VARSIZE(geom2) && !memcmp(geom1, geom2, VARSIZE(geom1))) {
	    PG_RETURN_BOOL(true);
	}

	initGEOS(lwpgnotice, lwgeom_geos_error);

	g1 = POSTGIS2GEOS(geom1);
	if (!g1) HANDLE_GEOS_ERROR("First argument geometry could not be converted to GEOS");
	g2 = POSTGIS2GEOS(geom2);
	if (!g2)
	{
		GEOSGeom_destroy(g1);
		HANDLE_GEOS_ERROR("Second argument geometry could not be converted to GEOS");
	}
	result = GEOSEquals(g1, g2);
	GEOSGeom_destroy(g1);
	GEOSGeom_destroy(g2);

	if (result == 2) HANDLE_GEOS_ERROR("GEOSEquals");

	PG_RETURN_BOOL(result);
}



PG_FUNCTION_INFO_V1(touches);
Datum touches(PG_FUNCTION_ARGS)
{
	SHARED_GSERIALIZED *shared_geom1 = ToastCacheGetGeometry(fcinfo, 0);
	SHARED_GSERIALIZED *shared_geom2 = ToastCacheGetGeometry(fcinfo, 1);
	const GSERIALIZED *geom1 = shared_gserialized_get(shared_geom1);
	const GSERIALIZED *geom2 = shared_gserialized_get(shared_geom2);
	int8_t result;
	GBOX box1, box2;
	PrepGeomCache *prep_cache;

	gserialized_error_if_srid_mismatch(geom1, geom2, __func__);

	/* A.Touches(Empty) == FALSE */
	if ( gserialized_is_empty(geom1) || gserialized_is_empty(geom2) )
		PG_RETURN_BOOL(false);

	/*
	 * Short-circuit 1: if geom2 bounding box does not overlap
	 * geom1 bounding box we can return FALSE.
	 */
	if ( gserialized_get_gbox_p(geom1, &box1) &&
	     gserialized_get_gbox_p(geom2, &box2) )
	{
		if ( gbox_overlaps_2d(&box1, &box2) == LW_FALSE )
		{
			PG_RETURN_BOOL(false);
		}
	}

	initGEOS(lwpgnotice, lwgeom_geos_error);

	prep_cache = GetPrepGeomCache(fcinfo, shared_geom1, shared_geom2);
	if ( prep_cache && prep_cache->prepared_geom )
	{
		GEOSGeometry *g = prep_cache->gcache.argnum == 1
			? POSTGIS2GEOS(geom2)
			: POSTGIS2GEOS(geom1);
		if (!g) HANDLE_GEOS_ERROR("Geometry could not be converted to GEOS");
		result = GEOSPreparedTouches(prep_cache->prepared_geom, g);
		GEOSGeom_destroy(g);
	}
	else
	{
		GEOSGeometry *g1, *g2;
		g1 = POSTGIS2GEOS(geom1);
		if (!g1) HANDLE_GEOS_ERROR("First argument geometry could not be converted to GEOS");
		g2 = POSTGIS2GEOS(geom2);
		if (!g2)
		{
			GEOSGeom_destroy(g1);
			HANDLE_GEOS_ERROR("Second argument geometry could not be converted to GEOS");
		}
		result = GEOSTouches(g1, g2);
		GEOSGeom_destroy(g1);
		GEOSGeom_destroy(g2);
	}

	if (result == 2) HANDLE_GEOS_ERROR("GEOSTouches");

	PG_RETURN_BOOL(result);
}


PG_FUNCTION_INFO_V1(disjoint);
Datum disjoint(PG_FUNCTION_ARGS)
{
	SHARED_GSERIALIZED *shared_geom1 = ToastCacheGetGeometry(fcinfo, 0);
	SHARED_GSERIALIZED *shared_geom2 = ToastCacheGetGeometry(fcinfo, 1);
	const GSERIALIZED *geom1 = shared_gserialized_get(shared_geom1);
	const GSERIALIZED *geom2 = shared_gserialized_get(shared_geom2);
	int8_t result;
	GBOX box1, box2;
	PrepGeomCache *prep_cache;
	bool pip_result;

	gserialized_error_if_srid_mismatch(geom1, geom2, __func__);

	/* A.Disjoint(Empty) == TRUE */
	if ( gserialized_is_empty(geom1) || gserialized_is_empty(geom2) )
		PG_RETURN_BOOL(true);

	/*
	 * Short-circuit 1: if geom2 bounding box does not overlap
	 * geom1 bounding box we can return TRUE.
	 */
	if ( gserialized_get_gbox_p(geom1, &box1) &&
	     gserialized_get_gbox_p(geom2, &box2) )
	{
		if ( gbox_overlaps_2d(&box1, &box2) == LW_FALSE )
		{
			PG_RETURN_BOOL(true);
		}
	}

	/*
	 * Short-circuit 2: if one geometry is polygonal and the other is a
	 * point or point-only collection, use the IntervalTree PIP fast path.
	 */
	if (is_point_or_collection(geom1) && is_poly(geom2) &&
	    try_itree_pointlike_pip(fcinfo, geom1, shared_geom2, itree_pip_intersects, &pip_result))
	{
		PG_RETURN_BOOL(!pip_result);
	}
	else if (is_point_or_collection(geom2) && is_poly(geom1) &&
		 try_itree_pointlike_pip(fcinfo, geom2, shared_geom1, itree_pip_intersects, &pip_result))
	{
		PG_RETURN_BOOL(!pip_result);
	}

	initGEOS(lwpgnotice, lwgeom_geos_error);

	prep_cache = GetPrepGeomCache(fcinfo, shared_geom1, shared_geom2);
	if ( prep_cache && prep_cache->prepared_geom )
	{
		GEOSGeometry *g = prep_cache->gcache.argnum == 1
			? POSTGIS2GEOS(geom2)
			: POSTGIS2GEOS(geom1);
		if (!g) HANDLE_GEOS_ERROR("Geometry could not be converted to GEOS");
		result = GEOSPreparedDisjoint(prep_cache->prepared_geom, g);
		GEOSGeom_destroy(g);
	}
	else
	{
		GEOSGeometry *g1, *g2;
		g1 = POSTGIS2GEOS(geom1);
		if (!g1) HANDLE_GEOS_ERROR("First argument geometry could not be converted to GEOS");
		g2 = POSTGIS2GEOS(geom2);
		if (!g2)
		{
			GEOSGeom_destroy(g1);
			HANDLE_GEOS_ERROR("Second argument geometry could not be converted to GEOS");
		}
		result = GEOSDisjoint(g1, g2);
		GEOSGeom_destroy(g1);
		GEOSGeom_destroy(g2);
	}

	if (result == 2) HANDLE_GEOS_ERROR("GEOSDisjoint");

	PG_RETURN_BOOL(result);
}



/**
 * ST_Overlaps(geometry, geometry)
 */
PG_FUNCTION_INFO_V1(overlaps);
Datum overlaps(PG_FUNCTION_ARGS)
{
	SHARED_GSERIALIZED *shared_geom1 = ToastCacheGetGeometry(fcinfo, 0);
	SHARED_GSERIALIZED *shared_geom2 = ToastCacheGetGeometry(fcinfo, 1);
	const GSERIALIZED *geom1 = shared_gserialized_get(shared_geom1);
	const GSERIALIZED *geom2 = shared_gserialized_get(shared_geom2);
	int8_t result;
	GBOX box1, box2;
	PrepGeomCache *prep_cache;

	gserialized_error_if_srid_mismatch(geom1, geom2, __func__);

	/* A.Overlaps(Empty) == FALSE */
	if ( gserialized_is_empty(geom1) || gserialized_is_empty(geom2) )
		PG_RETURN_BOOL(false);

	/*
	 * Short-circuit 1: if geom2 bounding box does not overlap
	 * geom1 bounding box we can return FALSE.
	 */
	if ( gserialized_get_gbox_p(geom1, &box1) &&
	     gserialized_get_gbox_p(geom2, &box2) )
	{
		if ( ! gbox_overlaps_2d(&box1, &box2) )
		{
			PG_RETURN_BOOL(false);
		}
	}

	initGEOS(lwpgnotice, lwgeom_geos_error);

	prep_cache = GetPrepGeomCache(fcinfo, shared_geom1, shared_geom2);
	if ( prep_cache && prep_cache->prepared_geom )
	{
		GEOSGeometry *g = prep_cache->gcache.argnum == 1
			? POSTGIS2GEOS(geom2)
			: POSTGIS2GEOS(geom1);
		if (!g) HANDLE_GEOS_ERROR("Geometry could not be converted to GEOS");
		result = GEOSPreparedOverlaps(prep_cache->prepared_geom, g);
		GEOSGeom_destroy(g);
	}
	else
	{
		GEOSGeometry *g1, *g2;
		g1 = POSTGIS2GEOS(geom1);
		if (!g1) HANDLE_GEOS_ERROR("First argument geometry could not be converted to GEOS");
		g2 = POSTGIS2GEOS(geom2);
		if (!g2)
		{
			GEOSGeom_destroy(g1);
			HANDLE_GEOS_ERROR("Second argument geometry could not be converted to GEOS");
		}
		result = GEOSOverlaps(g1, g2);
		GEOSGeom_destroy(g1);
		GEOSGeom_destroy(g2);
	}

	if (result == 2) HANDLE_GEOS_ERROR("GEOSOverlaps");

	PG_RETURN_BOOL(result);
}


/**
 * ST_Crosses(geometry, geometry)
 */
PG_FUNCTION_INFO_V1(crosses);
Datum crosses(PG_FUNCTION_ARGS)
{
	SHARED_GSERIALIZED *shared_geom1 = ToastCacheGetGeometry(fcinfo, 0);
	SHARED_GSERIALIZED *shared_geom2 = ToastCacheGetGeometry(fcinfo, 1);
	const GSERIALIZED *geom1 = shared_gserialized_get(shared_geom1);
	const GSERIALIZED *geom2 = shared_gserialized_get(shared_geom2);
	int8_t result;
	GBOX box1, box2;
	PrepGeomCache *prep_cache;

	gserialized_error_if_srid_mismatch(geom1, geom2, __func__);

	/* A.Crosses(Empty) == FALSE */
	if ( gserialized_is_empty(geom1) || gserialized_is_empty(geom2) )
		PG_RETURN_BOOL(false);

	/*
	 * Short-circuit 1: if geom2 bounding box does not overlap
	 * geom1 bounding box we can return FALSE.
	 */
	if ( gserialized_get_gbox_p(geom1, &box1) &&
	     gserialized_get_gbox_p(geom2, &box2) )
	{
		if ( gbox_overlaps_2d(&box1, &box2) == LW_FALSE )
		{
			PG_RETURN_BOOL(false);
		}
	}

	initGEOS(lwpgnotice, lwgeom_geos_error);

	prep_cache = GetPrepGeomCache(fcinfo, shared_geom1, shared_geom2);
	if ( prep_cache && prep_cache->prepared_geom )
	{
		GEOSGeometry *g = prep_cache->gcache.argnum == 1
			? POSTGIS2GEOS(geom2)
			: POSTGIS2GEOS(geom1);
		if (!g) HANDLE_GEOS_ERROR("Geometry could not be converted to GEOS");
		result = GEOSPreparedCrosses(prep_cache->prepared_geom, g);
		GEOSGeom_destroy(g);
	}
	else
	{
		GEOSGeometry *g1, *g2;
		g1 = POSTGIS2GEOS(geom1);
		if (!g1) HANDLE_GEOS_ERROR("First argument geometry could not be converted to GEOS");
		g2 = POSTGIS2GEOS(geom2);
		if (!g2)
		{
			GEOSGeom_destroy(g1);
			HANDLE_GEOS_ERROR("Second argument geometry could not be converted to GEOS");
		}
		result = GEOSCrosses(g1, g2);
		GEOSGeom_destroy(g1);
		GEOSGeom_destroy(g2);
	}

	if (result == 2) HANDLE_GEOS_ERROR("GEOSCrosses");

	PG_RETURN_BOOL(result);
}



/**
 * ST_Contains(geometry, geometry)
 */
PG_FUNCTION_INFO_V1(contains);
Datum contains(PG_FUNCTION_ARGS)
{
	SHARED_GSERIALIZED *shared_geom1 = ToastCacheGetGeometry(fcinfo, 0);
	SHARED_GSERIALIZED *shared_geom2 = ToastCacheGetGeometry(fcinfo, 1);
	const GSERIALIZED *geom1 = shared_gserialized_get(shared_geom1);
	const GSERIALIZED *geom2 = shared_gserialized_get(shared_geom2);
	int8_t result;
	GEOSGeometry *g1, *g2;
	GBOX box1, box2;
	PrepGeomCache *prep_cache;
	bool pip_result;

	gserialized_error_if_srid_mismatch(geom1, geom2, __func__);

	/* A.Contains(Empty) == FALSE */
	if (gserialized_is_empty(geom1) || gserialized_is_empty(geom2))
		PG_RETURN_BOOL(false);

	POSTGIS_DEBUGF(3, "Contains: type1: %d, type2: %d", gserialized_get_type(geom1), gserialized_get_type(geom2));

	/*
	** Short-circuit 1: if geom2 bounding box is not completely inside
	** geom1 bounding box we can return FALSE.
	*/
	if (gserialized_get_gbox_p(geom1, &box1) &&
	    gserialized_get_gbox_p(geom2, &box2))
	{
		if (!gbox_contains_2d(&box1, &box2))
			PG_RETURN_BOOL(false);
	}

	/*
	** Short-circuit 2: if geom1 is polygonal and geom2 is a point
	** or point-only collection, use the IntervalTree PIP fast path.
	*/
	if (is_poly(geom1) && is_point_or_collection(geom2) &&
	    try_itree_pointlike_pip(fcinfo, geom2, shared_geom1, itree_pip_contains, &pip_result))
	{
		PG_RETURN_BOOL(pip_result);
	}

	initGEOS(lwpgnotice, lwgeom_geos_error);

	prep_cache = GetPrepGeomCache(fcinfo, shared_geom1, NULL);
	if ( prep_cache && prep_cache->prepared_geom && prep_cache->gcache.argnum == 1 )
	{
		g1 = POSTGIS2GEOS(geom2);
		if (!g1)
			HANDLE_GEOS_ERROR("Geometry could not be converted to GEOS");

		result = GEOSPreparedContains( prep_cache->prepared_geom, g1);
		GEOSGeom_destroy(g1);
	}
	else
	{
		g1 = POSTGIS2GEOS(geom1);
		if (!g1) HANDLE_GEOS_ERROR("First argument geometry could not be converted to GEOS");
		g2 = POSTGIS2GEOS(geom2);
		if (!g2)
		{
			HANDLE_GEOS_ERROR("Second argument geometry could not be converted to GEOS");
			GEOSGeom_destroy(g1);
		}
		result = GEOSContains( g1, g2);
		GEOSGeom_destroy(g1);
		GEOSGeom_destroy(g2);
	}

	if (result == 2) HANDLE_GEOS_ERROR("GEOSContains");

	PG_RETURN_BOOL(result > 0);
}


/**
 * ST_Within(geometry, geometry)
 */
PG_FUNCTION_INFO_V1(within);
Datum within(PG_FUNCTION_ARGS)
{
	SHARED_GSERIALIZED *shared_geom1 = ToastCacheGetGeometry(fcinfo, 0);
	SHARED_GSERIALIZED *shared_geom2 = ToastCacheGetGeometry(fcinfo, 1);
	const GSERIALIZED *geom1 = shared_gserialized_get(shared_geom1);
	const GSERIALIZED *geom2 = shared_gserialized_get(shared_geom2);
	int8_t result;
	GEOSGeometry *g1, *g2;
	GBOX box1, box2;
	PrepGeomCache *prep_cache;
	bool pip_result;

	gserialized_error_if_srid_mismatch(geom1, geom2, __func__);

	/* A.Contains(Empty) == FALSE */
	if (gserialized_is_empty(geom1) || gserialized_is_empty(geom2))
		PG_RETURN_BOOL(false);

	POSTGIS_DEBUGF(3, "Contains: type1: %d, type2: %d", gserialized_get_type(geom1), gserialized_get_type(geom2));

	/*
	** Short-circuit 1: if geom1 bounding box is not completely inside
	** geom2 bounding box we can return FALSE.
	*/
	if (gserialized_get_gbox_p(geom1, &box1) &&
	    gserialized_get_gbox_p(geom2, &box2))
	{
		if (!gbox_within_2d(&box1, &box2))
			PG_RETURN_BOOL(false);
	}

	/*
	** Short-circuit 2: if geom2 is polygonal and geom1 is a point
	** or point-only collection, use the IntervalTree PIP fast path.
	*/
	if (is_poly(geom2) && is_point_or_collection(geom1) &&
	    try_itree_pointlike_pip(fcinfo, geom1, shared_geom2, itree_pip_contains, &pip_result))
	{
		PG_RETURN_BOOL(pip_result);
	}

	initGEOS(lwpgnotice, lwgeom_geos_error);

	prep_cache = GetPrepGeomCache(fcinfo, NULL, shared_geom2);
	if ( prep_cache && prep_cache->prepared_geom && prep_cache->gcache.argnum == 2 )
	{
		g1 = POSTGIS2GEOS(geom1);
		if (!g1)
			HANDLE_GEOS_ERROR("Geometry could not be converted to GEOS");

		result = GEOSPreparedContains(prep_cache->prepared_geom, g1);
		GEOSGeom_destroy(g1);
	}
	else
	{
		g1 = POSTGIS2GEOS(geom1);
		if (!g1) HANDLE_GEOS_ERROR("First argument geometry could not be converted to GEOS");
		g2 = POSTGIS2GEOS(geom2);
		if (!g2)
		{
			HANDLE_GEOS_ERROR("Second argument geometry could not be converted to GEOS");
			GEOSGeom_destroy(g1);
		}
		result = GEOSWithin(g1, g2);
		GEOSGeom_destroy(g1);
		GEOSGeom_destroy(g2);
	}

	if ( result == 2 ) HANDLE_GEOS_ERROR("GEOSWithin");

	PG_RETURN_BOOL(result > 0);
}

/**
 * ST_ContainsProperly(geometry, geometry)
 */
PG_FUNCTION_INFO_V1(containsproperly);
Datum containsproperly(PG_FUNCTION_ARGS)
{
	SHARED_GSERIALIZED *shared_geom1 = ToastCacheGetGeometry(fcinfo, 0);
	SHARED_GSERIALIZED *shared_geom2 = ToastCacheGetGeometry(fcinfo, 1);
	const GSERIALIZED *geom1 = shared_gserialized_get(shared_geom1);
	const GSERIALIZED *geom2 = shared_gserialized_get(shared_geom2);
	int8_t result;
	GBOX box1, box2;
	PrepGeomCache *prep_cache;

	gserialized_error_if_srid_mismatch(geom1, geom2, __func__);

	/* A.ContainsProperly(Empty) == FALSE */
	if ( gserialized_is_empty(geom1) || gserialized_is_empty(geom2) )
		PG_RETURN_BOOL(false);

	/*
	 * Short-circuit: if geom2 bounding box is not completely inside
	 * geom1 bounding box we can return FALSE.
	 */
	if ( gserialized_get_gbox_p(geom1, &box1) &&
	     gserialized_get_gbox_p(geom2, &box2) )
	{
		if ( ! gbox_contains_2d(&box1, &box2) )
			PG_RETURN_BOOL(false);
	}

	initGEOS(lwpgnotice, lwgeom_geos_error);

	prep_cache = GetPrepGeomCache(fcinfo, shared_geom1, 0);
	if ( prep_cache && prep_cache->prepared_geom && prep_cache->gcache.argnum == 1 )
	{
		GEOSGeometry *g = POSTGIS2GEOS(geom2);
		if (!g) HANDLE_GEOS_ERROR("First argument geometry could not be converted to GEOS");
		result = GEOSPreparedContainsProperly( prep_cache->prepared_geom, g);
		GEOSGeom_destroy(g);
	}
	else
	{
		GEOSGeometry *g1, *g2;
		g1 = POSTGIS2GEOS(geom1);
		if (!g1) HANDLE_GEOS_ERROR("First argument geometry could not be converted to GEOS");
		g2 = POSTGIS2GEOS(geom2);
		if (!g2)
		{
			GEOSGeom_destroy(g1);
			HANDLE_GEOS_ERROR("Second argument geometry could not be converted to GEOS");
		}
		result = GEOSRelatePattern(g1, g2, "T**FF*FF*");
		GEOSGeom_destroy(g1);
		GEOSGeom_destroy(g2);
	}

	if (result == 2) HANDLE_GEOS_ERROR("GEOSContainProperly");

	PG_RETURN_BOOL(result);
}


/**
 * ST_Covers(geometry, geometry)
 *
 * http://lin-ear-th-inking.blogspot.com/2007/06/subtleties-of-ogc-covers-spatial.html
 */
PG_FUNCTION_INFO_V1(covers);
Datum covers(PG_FUNCTION_ARGS)
{
	SHARED_GSERIALIZED *shared_geom1 = ToastCacheGetGeometry(fcinfo, 0);
	SHARED_GSERIALIZED *shared_geom2 = ToastCacheGetGeometry(fcinfo, 1);
	const GSERIALIZED *geom1 = shared_gserialized_get(shared_geom1);
	const GSERIALIZED *geom2 = shared_gserialized_get(shared_geom2);
	int8_t result;
	GBOX box1, box2;
	PrepGeomCache *prep_cache;
	bool pip_result;

	POSTGIS_DEBUGF(3, "Covers: type1: %d, type2: %d", gserialized_get_type(geom1), gserialized_get_type(geom2));

	/* A.Covers(Empty) == FALSE */
	if ( gserialized_is_empty(geom1) || gserialized_is_empty(geom2) )
		PG_RETURN_BOOL(false);

	gserialized_error_if_srid_mismatch(geom1, geom2, __func__);

	/*
	 * Short-circuit 1: if geom2 bounding box is not completely inside
	 * geom1 bounding box we can return FALSE.
	 */
	if ( gserialized_get_gbox_p(geom1, &box1) &&
	     gserialized_get_gbox_p(geom2, &box2) )
	{
		if ( ! gbox_contains_2d(&box1, &box2) )
		{
			PG_RETURN_BOOL(false);
		}
	}
	/*
	 * Short-circuit 2: if geom1 is polygonal and geom2 is a point
	 * or point-only collection, use the IntervalTree PIP fast path.
	 */
	if (is_poly(geom1) && is_point_or_collection(geom2) &&
	    try_itree_pointlike_pip(fcinfo, geom2, shared_geom1, itree_pip_covers, &pip_result))
	{
		PG_RETURN_BOOL(pip_result);
	}

	initGEOS(lwpgnotice, lwgeom_geos_error);

	prep_cache = GetPrepGeomCache(fcinfo, shared_geom1, 0);
	if ( prep_cache && prep_cache->prepared_geom && prep_cache->gcache.argnum == 1 )
	{
		GEOSGeometry *g1 = POSTGIS2GEOS(geom2);
		if (!g1) HANDLE_GEOS_ERROR("First argument geometry could not be converted to GEOS");
		result = GEOSPreparedCovers( prep_cache->prepared_geom, g1);
		GEOSGeom_destroy(g1);
	}
	else
	{
		GEOSGeometry *g1, *g2;

		g1 = POSTGIS2GEOS(geom1);
		if (!g1) HANDLE_GEOS_ERROR("First argument geometry could not be converted to GEOS");
		g2 = POSTGIS2GEOS(geom2);
		if (!g2)
		{
			GEOSGeom_destroy(g1);
			HANDLE_GEOS_ERROR("Second argument geometry could not be converted to GEOS");
		}
		result = GEOSRelatePattern( g1, g2, "******FF*" );
		GEOSGeom_destroy(g1);
		GEOSGeom_destroy(g2);
	}

	if (result == 2) HANDLE_GEOS_ERROR("GEOSCovers");

	PG_RETURN_BOOL(result);
}


/**
 * ST_CoveredBy(geometry, geometry)
 *
 * Described at:
 * http://lin-ear-th-inking.blogspot.com/2007/06/subtleties-of-ogc-covers-spatial.html
 */
PG_FUNCTION_INFO_V1(coveredby);
Datum coveredby(PG_FUNCTION_ARGS)
{
	SHARED_GSERIALIZED *shared_geom1 = ToastCacheGetGeometry(fcinfo, 0);
	SHARED_GSERIALIZED *shared_geom2 = ToastCacheGetGeometry(fcinfo, 1);
	const GSERIALIZED *geom1 = shared_gserialized_get(shared_geom1);
	const GSERIALIZED *geom2 = shared_gserialized_get(shared_geom2);
	int8_t result;
	GBOX box1, box2;
	PrepGeomCache *prep_cache;
	bool pip_result;

	gserialized_error_if_srid_mismatch(geom1, geom2, __func__);

	POSTGIS_DEBUGF(3, "CoveredBy: type1: %d, type2: %d", gserialized_get_type(geom1), gserialized_get_type(geom2));

	/* A.CoveredBy(Empty) == FALSE */
	if ( gserialized_is_empty(geom1) || gserialized_is_empty(geom2) )
		PG_RETURN_BOOL(false);

	/*
	 * Short-circuit 1: if geom1 bounding box is not completely inside
	 * geom2 bounding box we can return FALSE.
	 */
	if ( gserialized_get_gbox_p(geom1, &box1) &&
	     gserialized_get_gbox_p(geom2, &box2) )
	{
		if ( ! gbox_within_2d(&box1, &box2) )
		{
			PG_RETURN_BOOL(false);
		}
	}
	/*
	 * Short-circuit 2: if geom2 is polygonal and geom1 is a point
	 * or point-only collection, use the IntervalTree PIP fast path.
	 */
	if (is_point_or_collection(geom1) && is_poly(geom2) &&
	    try_itree_pointlike_pip(fcinfo, geom1, shared_geom2, itree_pip_covers, &pip_result))
	{
		PG_RETURN_BOOL(pip_result);
	}

	initGEOS(lwpgnotice, lwgeom_geos_error);

	prep_cache = GetPrepGeomCache(fcinfo, shared_geom1, 0);
	if ( prep_cache && prep_cache->prepared_geom && prep_cache->gcache.argnum == 2)
	{
		GEOSGeometry *g1 = POSTGIS2GEOS(geom2);
		if (!g1) HANDLE_GEOS_ERROR("First argument geometry could not be converted to GEOS");
		result = GEOSPreparedCovers( prep_cache->prepared_geom, g1);
		GEOSGeom_destroy(g1);
	}
	else
	{
		GEOSGeometry *g1, *g2;
		g1 = POSTGIS2GEOS(geom1);
		if (!g1) HANDLE_GEOS_ERROR("First argument geometry could not be converted to GEOS");

		g2 = POSTGIS2GEOS(geom2);
		if (!g2)
		{
			GEOSGeom_destroy(g1);
			HANDLE_GEOS_ERROR("Second argument geometry could not be converted to GEOS");
		}

		result = GEOSCoveredBy(g1, g2);
		GEOSGeom_destroy(g1);
		GEOSGeom_destroy(g2);
	}

	if (result == 2) HANDLE_GEOS_ERROR("GEOSCoveredBy");

	PG_RETURN_BOOL(result);
}

#if POSTGIS_GEOS_VERSION >= 31300
/*
 *  Flip the DE9IM matrix around the diagonal to
 *  account for flipping the order of the operands
 *  to the GEOSPreparedRelatePattern function.
 *
 *  Short patterns are invalid for GEOS, but they can still reach this helper
 *  before GEOS reports the error, so every swapped position must be guarded by
 *  the actual pattern length.
 */
static void
imInvert(char *im, size_t len)
{
	char t;
	if (len > 3)
	{
		t = im[1];
		im[1] = im[3];
		im[3] = t;
	}
	if (len > 6)
	{
		t = im[2];
		im[2] = im[6];
		im[6] = t;
	}
	if (len > 7)
	{
		t = im[5];
		im[5] = im[7];
		im[7] = t;
	}
}
#endif

PG_FUNCTION_INFO_V1(relate_pattern);
Datum relate_pattern(PG_FUNCTION_ARGS)
{
	SHARED_GSERIALIZED *shared_geom1 = ToastCacheGetGeometry(fcinfo, 0);
	SHARED_GSERIALIZED *shared_geom2 = ToastCacheGetGeometry(fcinfo, 1);
	const GSERIALIZED *geom1 = shared_gserialized_get(shared_geom1);
	const GSERIALIZED *geom2 = shared_gserialized_get(shared_geom2);

	/* Ensure DE9IM pattern is no more than 9 chars */
	text *imPtr = DatumGetTextP(DirectFunctionCall2(text_left,
		PG_GETARG_DATUM(2), Int32GetDatum(9)));
	char *im = text_to_cstring(imPtr);
	int8_t result;
	size_t im_len = strlen(im);
	size_t i;
#if POSTGIS_GEOS_VERSION >= 31300
	PrepGeomCache *prep_cache;
#endif

	gserialized_error_if_srid_mismatch(geom1, geom2, __func__);

	/*
	** Need to make sure 't' and 'f' are upper-case before handing to GEOS
	*/
	for (i = 0; i < im_len; i++)
		im[i] = toupper((unsigned char)im[i]);

	initGEOS(lwpgnotice, lwgeom_geos_error);

#if POSTGIS_GEOS_VERSION >= 31300
	prep_cache = GetPrepGeomCache(fcinfo, shared_geom1, shared_geom2);
	if ( prep_cache && prep_cache->prepared_geom )
	{
		GEOSGeometry *g = NULL;
		if (prep_cache->gcache.argnum == 1)
		{
			g = POSTGIS2GEOS(geom2);
		}
		else
		{
			g = POSTGIS2GEOS(geom1);
			imInvert(im, im_len);
		}

		if (!g) HANDLE_GEOS_ERROR("Geometry could not be converted to GEOS");
		result = GEOSPreparedRelatePattern(prep_cache->prepared_geom, g, im);
		GEOSGeom_destroy(g);
	}
	else
#endif
	{
		GEOSGeometry *g1, *g2;
		g1 = POSTGIS2GEOS(geom1);
		if (!g1) HANDLE_GEOS_ERROR("First argument geometry could not be converted to GEOS");
		g2 = POSTGIS2GEOS(geom2);
		if (!g2)
		{
			HANDLE_GEOS_ERROR("Second argument geometry could not be converted to GEOS");
			GEOSGeom_destroy(g1);
		}
		result = GEOSRelatePattern(g1, g2, im);
		GEOSGeom_destroy(g1);
		GEOSGeom_destroy(g2);
	}

	pfree(im);
	if (result == 2) HANDLE_GEOS_ERROR("GEOSRelatePattern");

	PG_RETURN_BOOL(result);
}


PG_FUNCTION_INFO_V1(relate_full);
Datum relate_full(PG_FUNCTION_ARGS)
{
	GSERIALIZED *geom1 = PG_GETARG_GSERIALIZED_P(0);
	GSERIALIZED *geom2 = PG_GETARG_GSERIALIZED_P(1);
	GEOSGeometry *g1, *g2;
	char *relate_str;
	text *result;
	int bnr = GEOSRELATE_BNR_OGC;

	/* TODO handle empty */
	gserialized_error_if_srid_mismatch(geom1, geom2, __func__);

	if ( PG_NARGS() > 2 )
		bnr = PG_GETARG_INT32(2);

	initGEOS(lwpgnotice, lwgeom_geos_error);

	g1 = POSTGIS2GEOS(geom1);
	if (!g1)
		HANDLE_GEOS_ERROR("First argument geometry could not be converted to GEOS");
	g2 = POSTGIS2GEOS(geom2);
	if (!g2)
	{
		GEOSGeom_destroy(g1);
		HANDLE_GEOS_ERROR("Second argument geometry could not be converted to GEOS");
	}

	POSTGIS_DEBUG(3, "constructed geometries ");

	POSTGIS_DEBUGF(3, "%s", GEOSGeomToWKT(g1));
	POSTGIS_DEBUGF(3, "%s", GEOSGeomToWKT(g2));

	relate_str = GEOSRelateBoundaryNodeRule(g1, g2, bnr);
	if (!relate_str) {
		GEOSGeom_destroy(g1);
		GEOSGeom_destroy(g2);
		HANDLE_GEOS_ERROR("GEOSRelate");
	}
	result = cstring_to_text(relate_str);
	GEOSFree(relate_str);
	GEOSGeom_destroy(g1);
	GEOSGeom_destroy(g2);

	PG_FREE_IF_COPY(geom1, 0);
	PG_FREE_IF_COPY(geom2, 1);

	PG_RETURN_TEXT_P(result);
}


/*
 * ST_DWithin(geom1, geom2, radius) returns boolean
 *
 * For GEOS >= 3.10, this is a cached predicate implementation much
 * like the other functions in this module. As with the other prepared
 * predicates, the utility of the caching/preparation is a function
 * of the size of the inputs, in particular the cached input, and
 * that caching only happens if the input shows up multiple times in
 * a row, which mostly is a result of either a literal or a
 * nested loop join. The size of the inputs is used as an initial
 * pre-check on whether to engage the geos/prepared geometry
 * machinery.
 */
PG_FUNCTION_INFO_V1(LWGEOM_dwithin);
Datum LWGEOM_dwithin(PG_FUNCTION_ARGS)
{

	double tolerance = PG_GETARG_FLOAT8(2);
	double mindist;

	SHARED_GSERIALIZED *shared_geom1 = ToastCacheGetGeometry(fcinfo, 0);
	SHARED_GSERIALIZED *shared_geom2 = ToastCacheGetGeometry(fcinfo, 1);
	const GSERIALIZED *geom1 = shared_gserialized_get(shared_geom1);
	const GSERIALIZED *geom2 = shared_gserialized_get(shared_geom2);
	PrepGeomCache *prep_cache = NULL;
	const size_t small_threshold = 1024;
	int8_t is_dwithin = -1;

	/*
	 * Only enter the GEOS/PreparedGeometry code line if one of the
	 * operands is large enough that the win from indexing will exceed
	 * the loss from GEOS fixed overhead.
	 */
	bool use_prepared =
		(LWSIZE_GET(geom1->size) > small_threshold) ||
		(LWSIZE_GET(geom2->size) > small_threshold);

	if (tolerance < 0)
		elog(ERROR, "Tolerance cannot be less than zero");

	/*
	 * Error out early for mismatched SRID or empty inputs.
	 */
	gserialized_error_if_srid_mismatch(geom1, geom2, __func__);
	if (gserialized_is_empty(geom1) || gserialized_is_empty(geom2))
		PG_RETURN_BOOL(false);

	/*
	 * Only enter GEOS/PreparedGeometry code line if objects
	 * are big enough and cache is populated.
	 */
	if (use_prepared)
	{
		initGEOS(lwpgnotice, lwgeom_geos_error);
		prep_cache = GetPrepGeomCache(fcinfo, shared_geom1, shared_geom2);
		if (prep_cache && prep_cache->prepared_geom)
		{
			GEOSGeometry *g = NULL;
			if (prep_cache->gcache.argnum == 1)
				g = POSTGIS2GEOS(geom2);
			else
				g = POSTGIS2GEOS(geom1);

			if (!g) HANDLE_GEOS_ERROR("Geometry could not be converted to GEOS");
			is_dwithin = GEOSPreparedDistanceWithin(prep_cache->prepared_geom, g, tolerance);
			if (is_dwithin == 2) HANDLE_GEOS_ERROR("GEOSPreparedDistanceWithin");
			GEOSGeom_destroy(g);
		}
	}

	/*
	 * If for any reason we did not use GEOS/PreparedGeometry, we still
	 * need an answer, so use the brute force implementation.
	 */
	if (is_dwithin < 0)
	{
		LWGEOM *lwgeom1 = lwgeom_from_gserialized(geom1);
		LWGEOM *lwgeom2 = lwgeom_from_gserialized(geom2);
		mindist = lwgeom_mindistance2d_tolerance(lwgeom1, lwgeom2, tolerance);
		lwgeom_free(lwgeom1);
		lwgeom_free(lwgeom2);
		is_dwithin = (tolerance >= mindist);
	}

	PG_RETURN_BOOL(is_dwithin);
}
