/***********************************************************************
* pc_access.c
*
*  Accessor/aggregate functions for points and patches in PgSQL.
*
*  PgSQL Pointcloud is free and open source software provided
*  by the Government of Canada
*  Copyright (c) 2013 Natural Resources Canada
*
***********************************************************************/

#include "pc_pgsql.h"      /* Common PgSQL support for our type */
#include "utils/numeric.h"
#include "funcapi.h"
#include "lib/stringinfo.h"
#include "pc_api_internal.h" /* for pcpatch_summary */

/* cstring array utility functions */
const char **array_to_cstring_array(ArrayType *array, int *size);
void pc_cstring_array_free(const char **array, int nelems);

/* General SQL functions */
Datum pcpoint_get_value(PG_FUNCTION_ARGS);
Datum pcpoint_get_values(PG_FUNCTION_ARGS);
Datum pcpatch_from_pcpoint_array(PG_FUNCTION_ARGS);
Datum pcpatch_from_float_array(PG_FUNCTION_ARGS);
Datum pcpatch_from_pcpatch_array(PG_FUNCTION_ARGS);
Datum pcpatch_uncompress(PG_FUNCTION_ARGS);
Datum pcpatch_compress(PG_FUNCTION_ARGS);
Datum pcpatch_numpoints(PG_FUNCTION_ARGS);
Datum pcpatch_pointn(PG_FUNCTION_ARGS);
Datum pcpatch_range(PG_FUNCTION_ARGS);
Datum pcpatch_pcid(PG_FUNCTION_ARGS);
Datum pcpatch_summary(PG_FUNCTION_ARGS);
Datum pcpatch_compression(PG_FUNCTION_ARGS);
Datum pcpatch_intersects(PG_FUNCTION_ARGS);
Datum pcpatch_get_stat(PG_FUNCTION_ARGS);
Datum pcpatch_filter(PG_FUNCTION_ARGS);
Datum pcpatch_sort(PG_FUNCTION_ARGS);
Datum pcpatch_is_sorted(PG_FUNCTION_ARGS);
Datum pcpatch_size(PG_FUNCTION_ARGS);
Datum pcpoint_size(PG_FUNCTION_ARGS);
Datum pcpoint_pcid(PG_FUNCTION_ARGS);
Datum pc_version(PG_FUNCTION_ARGS);
Datum pc_pgsql_version(PG_FUNCTION_ARGS);
Datum pc_libxml2_version(PG_FUNCTION_ARGS);
Datum pc_lazperf_enabled(PG_FUNCTION_ARGS);

/* Generic aggregation functions */
Datum pointcloud_agg_transfn(PG_FUNCTION_ARGS);
Datum pointcloud_abs_in(PG_FUNCTION_ARGS);
Datum pointcloud_abs_out(PG_FUNCTION_ARGS);

/* Point finalizers */
Datum pcpoint_agg_final_pcpatch(PG_FUNCTION_ARGS);
Datum pcpoint_agg_final_array(PG_FUNCTION_ARGS);

/* Patch finalizers */
Datum pcpatch_agg_final_array(PG_FUNCTION_ARGS);
Datum pcpatch_agg_final_pcpatch(PG_FUNCTION_ARGS);

/* Deaggregation functions */
Datum pcpatch_unnest(PG_FUNCTION_ARGS);

/**
* Read a named dimension from a PCPOINT
* PC_Get(point pcpoint, dimname text) returns Numeric
*/
PG_FUNCTION_INFO_V1(pcpoint_get_value);
Datum pcpoint_get_value(PG_FUNCTION_ARGS)
{
	SERIALIZED_POINT *serpt = PG_GETARG_SERPOINT_P(0);
	text *dim_name = PG_GETARG_TEXT_P(1);
	char *dim_str;
	float8 double_result;

	PCSCHEMA *schema = pc_schema_from_pcid(serpt->pcid, fcinfo);
	PCPOINT *pt = pc_point_deserialize(serpt, schema);
	if ( ! pt )
		PG_RETURN_NULL();

	dim_str = text_to_cstring(dim_name);
	if ( ! pc_point_get_double_by_name(pt, dim_str, &double_result) )
	{
		pc_point_free(pt);
		elog(ERROR, "dimension \"%s\" does not exist in schema", dim_str);
	}
	pfree(dim_str);
	pc_point_free(pt);
	PG_RETURN_DATUM(DirectFunctionCall1(float8_numeric, Float8GetDatum(double_result)));
}

/**
* Returns all the values of a point as a double precision array
* PC_Get(point pcpoint) returns Float8[]
*/
PG_FUNCTION_INFO_V1(pcpoint_get_values);
Datum pcpoint_get_values(PG_FUNCTION_ARGS)
{
	SERIALIZED_POINT *serpt;
	ArrayType  *result;
	PCSCHEMA *schema;
	PCPOINT *pt;
	Datum *elems;
	int i;
	double *vals;

	serpt = PG_GETARG_SERPOINT_P(0);
	schema = pc_schema_from_pcid(serpt->pcid, fcinfo);
	pt = pc_point_deserialize(serpt, schema);
	if ( ! pt ) PG_RETURN_NULL();

	elems = (Datum * )palloc(schema->ndims * sizeof(Datum) );
	vals = pc_point_to_double_array(pt);
	i = schema->ndims;
	while (i--) elems[i] = Float8GetDatum(vals[i]);
	pcfree(vals);
	result = construct_array(elems, schema->ndims, FLOAT8OID,
		sizeof(float8), FLOAT8PASSBYVAL, 'd');

	pc_point_free(pt);
	PG_RETURN_ARRAYTYPE_P(result);
}


static inline bool
array_get_isnull(const bits8 *nullbitmap, int offset)
{
	if (nullbitmap == NULL)
	{
		return false; /* assume not null */
	}
	if (nullbitmap[offset / 8] & (1 << (offset % 8)))
	{
		return false; /* not null */
	}
	return true;
}

static PCPATCH *
#if PGSQL_VERSION < 120
pcpatch_from_point_array(ArrayType *array, FunctionCallInfoData *fcinfo)
#else
pcpatch_from_point_array(ArrayType *array, FunctionCallInfo fcinfo)
#endif
{
	int nelems;
	bits8 *bitmap;
	size_t offset = 0;
	int i;
	uint32 pcid = 0;
	PCPATCH *pa;
	PCPOINTLIST *pl;
	PCSCHEMA *schema = 0;

	/* How many things in our array? */
	nelems = ArrayGetNItems(ARR_NDIM(array), ARR_DIMS(array));

	/* PgSQL supplies a bitmap of which array entries are null */
	bitmap = ARR_NULLBITMAP(array);

	/* Empty array? Null return */
	if ( nelems == 0 )
		return NULL;

	/* Make our holder */
	pl = pc_pointlist_make(nelems);

	offset = 0;
	bitmap = ARR_NULLBITMAP(array);
	for ( i = 0; i < nelems; i++ )
	{
		/* Only work on non-NULL entries in the array */
		if ( ! array_get_isnull(bitmap, i) )
		{
			SERIALIZED_POINT *serpt = (SERIALIZED_POINT *)(ARR_DATA_PTR(array)+offset);
			PCPOINT *pt;

			if ( ! schema )
			{
				schema = pc_schema_from_pcid(serpt->pcid, fcinfo);
			}

			if ( ! pcid )
			{
				pcid = serpt->pcid;
			}
			else if ( pcid != serpt->pcid )
			{
				elog(ERROR, "pcpatch_from_point_array: pcid mismatch (%d != %d)", serpt->pcid, pcid);
			}

			pt = pc_point_deserialize(serpt, schema);
			if ( ! pt )
			{
				elog(ERROR, "pcpatch_from_point_array: point deserialization failed");
			}

			pc_pointlist_add_point(pl, pt);

			offset += INTALIGN(VARSIZE(serpt));
		}

	}

	if ( pl->npoints == 0 )
		return NULL;

	pa = pc_patch_from_pointlist(pl);
	pc_pointlist_free(pl);
	return pa;
}


static PCPATCH *
#if PGSQL_VERSION < 120
pcpatch_from_patch_array(ArrayType *array, FunctionCallInfoData *fcinfo)
#else
pcpatch_from_patch_array(ArrayType *array, FunctionCallInfo fcinfo)
#endif
{
	int nelems;
	bits8 *bitmap;
	size_t offset = 0;
	int i;
	uint32 pcid = 0;
	PCPATCH *pa;
	PCPATCH **palist;
	int numpatches = 0;
	PCSCHEMA *schema = 0;

	/* How many things in our array? */
	nelems = ArrayGetNItems(ARR_NDIM(array), ARR_DIMS(array));

	/* PgSQL supplies a bitmap of which array entries are null */
	bitmap = ARR_NULLBITMAP(array);

	/* Empty array? Null return */
	if ( nelems == 0 )
		return NULL;

	/* Make our temporary list of patches */
	palist = pcalloc(nelems*sizeof(PCPATCH*));

	/* Read the patches out of the array and deserialize */
	offset = 0;
	bitmap = ARR_NULLBITMAP(array);
	for ( i = 0; i < nelems; i++ )
	{
		/* Only work on non-NULL entries in the array */
		if ( ! array_get_isnull(bitmap, i) )
		{
			SERIALIZED_PATCH *serpatch = (SERIALIZED_PATCH *)(ARR_DATA_PTR(array)+offset);

			if ( ! schema )
			{
				schema = pc_schema_from_pcid(serpatch->pcid, fcinfo);
			}

			if ( ! pcid )
			{
				pcid = serpatch->pcid;
			}
			else if ( pcid != serpatch->pcid )
			{
				elog(ERROR, "pcpatch_from_patch_array: pcid mismatch (%d != %d)", serpatch->pcid, pcid);
			}

			pa = pc_patch_deserialize(serpatch, schema);
			if ( ! pa )
			{
				elog(ERROR, "pcpatch_from_patch_array: patch deserialization failed");
			}

			palist[numpatches++] = pa;

			offset += INTALIGN(VARSIZE(serpatch));
		}

	}

	/* Can't do anything w/ NULL */
	if ( numpatches == 0 )
		return NULL;

	/* Pass to the lib to build the output patch from the list */
	pa = pc_patch_from_patchlist(palist, numpatches);

	/* Free the temporary patch list */
	for ( i = 0; i < numpatches; i++ )
	{
		pc_patch_free(palist[i]);
	}
	pcfree(palist);

	return pa;
}


PG_FUNCTION_INFO_V1(pcpatch_from_pcpatch_array);
Datum pcpatch_from_pcpatch_array(PG_FUNCTION_ARGS)
{
	ArrayType *array;
	PCPATCH *pa;
	SERIALIZED_PATCH *serpa;

	if ( PG_ARGISNULL(0) )
		PG_RETURN_NULL();

	array = DatumGetArrayTypeP(PG_GETARG_DATUM(0));
	pa = pcpatch_from_patch_array(array, fcinfo);
	if ( ! pa )
		PG_RETURN_NULL();

	serpa = pc_patch_serialize(pa, NULL);
	pc_patch_free(pa);
	PG_RETURN_POINTER(serpa);
}

PG_FUNCTION_INFO_V1(pcpatch_from_pcpoint_array);
Datum pcpatch_from_pcpoint_array(PG_FUNCTION_ARGS)
{
	ArrayType *array;
	PCPATCH *pa;
	SERIALIZED_PATCH *serpa;

	if ( PG_ARGISNULL(0) )
		PG_RETURN_NULL();

	array = DatumGetArrayTypeP(PG_GETARG_DATUM(0));
	pa = pcpatch_from_point_array(array, fcinfo);
	if ( ! pa )
		PG_RETURN_NULL();

	serpa = pc_patch_serialize(pa, NULL);
	pc_patch_free(pa);
	PG_RETURN_POINTER(serpa);
}


PG_FUNCTION_INFO_V1(pcpatch_from_float_array);
Datum pcpatch_from_float_array(PG_FUNCTION_ARGS)
{
	int i, ndims, nelems, npoints;
	float8 *vals;
	PCPATCH *pa;
	PCPOINTLIST *pl;
	SERIALIZED_PATCH *serpa;
	uint32 pcid = PG_GETARG_INT32(0);
	ArrayType *arrptr = PG_GETARG_ARRAYTYPE_P(1);
	PCSCHEMA *schema = pc_schema_from_pcid(pcid, fcinfo);

	if ( ! schema )
		elog(ERROR, "unable to load schema for pcid = %d", pcid);

	if ( ARR_ELEMTYPE(arrptr) != FLOAT8OID )
		elog(ERROR, "array must be of float8[]");

	if ( ARR_NDIM(arrptr) != 1 )
		elog(ERROR, "float8[] must have one dimension");

	if ( ARR_HASNULL(arrptr) )
		elog(ERROR, "float8[] must not have null elements");

	ndims = schema->ndims;
	nelems = ARR_DIMS(arrptr)[0];

	if ( nelems % ndims != 0 ) {
		elog(ERROR, "array dimensions do not match schema dimensions of pcid = %d", pcid);
	}

	npoints = nelems / ndims;

	vals = (float8*) ARR_DATA_PTR(arrptr);
	pl = pc_pointlist_make(nelems);

	for ( i = 0; i < npoints; ++i ) {

		PCPOINT* pt = pc_point_from_double_array(schema, vals, i * ndims, ndims);
		pc_pointlist_add_point(pl, pt);
	}

	pa = pc_patch_from_pointlist(pl);
	pc_pointlist_free(pl);
	if ( ! pa )
		PG_RETURN_NULL();

	serpa = pc_patch_serialize(pa, NULL);

	pc_patch_free(pa);
	PG_RETURN_POINTER(serpa);
}

typedef struct
{
	ArrayBuildState *s;
} abs_trans;

PG_FUNCTION_INFO_V1(pointcloud_abs_in);
Datum pointcloud_abs_in(PG_FUNCTION_ARGS)
{
	ereport(ERROR,(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
		errmsg("function pointcloud_abs_in not implemented")));
	PG_RETURN_POINTER(NULL);
}

PG_FUNCTION_INFO_V1(pointcloud_abs_out);
Datum pointcloud_abs_out(PG_FUNCTION_ARGS)
{
	ereport(ERROR,(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
		errmsg("function pointcloud_abs_out not implemented")));
	PG_RETURN_POINTER(NULL);
}


PG_FUNCTION_INFO_V1(pointcloud_agg_transfn);
Datum pointcloud_agg_transfn(PG_FUNCTION_ARGS)
{
	Oid arg1_typeid = get_fn_expr_argtype(fcinfo->flinfo, 1);
	MemoryContext aggcontext;
	abs_trans *a;
	ArrayBuildState *state;
	Datum elem;

	if (arg1_typeid == InvalidOid)
		ereport(ERROR,
			(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
			errmsg("could not determine input data type")));

	if ( ! AggCheckCallContext(fcinfo, &aggcontext) )
	{
		/* cannot be called directly because of dummy-type argument */
		elog(ERROR, "pointcloud_agg_transfn called in non-aggregate context");
		aggcontext = NULL;  /* keep compiler quiet */
	}

	if ( PG_ARGISNULL(0) )
	{
		a = (abs_trans*) palloc(sizeof(abs_trans));
		a->s = NULL;
	}
	else
	{
		a = (abs_trans*) PG_GETARG_POINTER(0);
	}
	state = a->s;
	elem = PG_ARGISNULL(1) ? (Datum) 0 : PG_GETARG_DATUM(1);
	state = accumArrayResult(state,
		elem,
		PG_ARGISNULL(1),
		arg1_typeid,
		aggcontext);
	a->s = state;

	PG_RETURN_POINTER(a);
}




static Datum
pointcloud_agg_final(abs_trans *a, MemoryContext mctx, FunctionCallInfo fcinfo)
{
	ArrayBuildState *state;
	int dims[1];
	int lbs[1];
	state = a->s;
	dims[0] = state->nelems;
	lbs[0] = 1;
	return makeMdArrayResult(state, 1, dims, lbs, mctx, false);
}

PG_FUNCTION_INFO_V1(pcpoint_agg_final_array);
Datum pcpoint_agg_final_array(PG_FUNCTION_ARGS)
{
	abs_trans *a;
	Datum result = 0;

	if (PG_ARGISNULL(0))
		PG_RETURN_NULL();   /* returns null iff no input values */

	a = (abs_trans*) PG_GETARG_POINTER(0);

	result = pointcloud_agg_final(a, CurrentMemoryContext, fcinfo);
	PG_RETURN_DATUM(result);
}


PG_FUNCTION_INFO_V1(pcpatch_agg_final_array);
Datum pcpatch_agg_final_array(PG_FUNCTION_ARGS)
{
	abs_trans *a;
	Datum result = 0;

	if (PG_ARGISNULL(0))
		PG_RETURN_NULL();   /* returns null iff no input values */

	a = (abs_trans*) PG_GETARG_POINTER(0);

	result = pointcloud_agg_final(a, CurrentMemoryContext, fcinfo);
	PG_RETURN_DATUM(result);
}


PG_FUNCTION_INFO_V1(pcpoint_agg_final_pcpatch);
Datum pcpoint_agg_final_pcpatch(PG_FUNCTION_ARGS)
{
	ArrayType *array;
	abs_trans *a;
	PCPATCH *pa;
	SERIALIZED_PATCH *serpa;

	if (PG_ARGISNULL(0))
		PG_RETURN_NULL();   /* returns null iff no input values */

	a = (abs_trans*) PG_GETARG_POINTER(0);

	array = DatumGetArrayTypeP(pointcloud_agg_final(a, CurrentMemoryContext, fcinfo));
	pa = pcpatch_from_point_array(array, fcinfo);
	if ( ! pa )
		PG_RETURN_NULL();

	serpa = pc_patch_serialize(pa, NULL);
	pc_patch_free(pa);
	PG_RETURN_POINTER(serpa);
}


PG_FUNCTION_INFO_V1(pcpatch_agg_final_pcpatch);
Datum pcpatch_agg_final_pcpatch(PG_FUNCTION_ARGS)
{
	ArrayType *array;
	abs_trans *a;
	PCPATCH *pa;
	SERIALIZED_PATCH *serpa;

	if (PG_ARGISNULL(0))
		PG_RETURN_NULL();   /* returns null iff no input values */

	a = (abs_trans*) PG_GETARG_POINTER(0);

	array = DatumGetArrayTypeP(pointcloud_agg_final(a, CurrentMemoryContext, fcinfo));
	pa = pcpatch_from_patch_array(array, fcinfo);
	if ( ! pa )
		PG_RETURN_NULL();

	serpa = pc_patch_serialize(pa, NULL);
	pc_patch_free(pa);
	PG_RETURN_POINTER(serpa);
}


PG_FUNCTION_INFO_V1(pcpatch_unnest);
Datum pcpatch_unnest(PG_FUNCTION_ARGS)
{
	typedef struct
	{
		int nextelem;
		int numelems;
		PCPOINTLIST *pointlist;
	} pcpatch_unnest_fctx;

	FuncCallContext *funcctx;
	pcpatch_unnest_fctx *fctx;
	MemoryContext oldcontext;

	/* stuff done only on the first call of the function */
	if (SRF_IS_FIRSTCALL())
	{
		PCPATCH *patch;
		SERIALIZED_PATCH *serpatch;

		/* create a function context for cross-call persistence */
		funcctx = SRF_FIRSTCALL_INIT();

		/*
		* switch to memory context appropriate for multiple function calls
		*/
		oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);

		/*
		* Get the patch value and detoast if needed.  We can't do this
		* earlier because if we have to detoast, we want the detoasted copy
		* to be in multi_call_memory_ctx, so it will go away when we're done
		* and not before.      (If no detoast happens, we assume the originally
		* passed array will stick around till then.)
		*/
		serpatch = PG_GETARG_SERPATCH_P(0);
		patch = pc_patch_deserialize(serpatch, pc_schema_from_pcid_uncached(serpatch->pcid));

		/* allocate memory for user context */
		fctx = (pcpatch_unnest_fctx *) palloc(sizeof(pcpatch_unnest_fctx));

		/* initialize state */
		fctx->nextelem = 0;
		fctx->numelems = patch->npoints;
		fctx->pointlist = pc_pointlist_from_patch(patch);

		/* save user context, switch back to function context */
		funcctx->user_fctx = fctx;
		MemoryContextSwitchTo(oldcontext);
	}

	/* stuff done on every call of the function */
	funcctx = SRF_PERCALL_SETUP();
	fctx = funcctx->user_fctx;

	if (fctx->nextelem < fctx->numelems)
	{
		Datum elem;
		PCPOINT *pt = pc_pointlist_get_point(fctx->pointlist, fctx->nextelem);
		SERIALIZED_POINT *serpt = pc_point_serialize(pt);
		fctx->nextelem++;
		elem = PointerGetDatum(serpt);
		SRF_RETURN_NEXT(funcctx, elem);
	}
	else
	{
		/* do when there is no more left */
		SRF_RETURN_DONE(funcctx);
	}
}

PG_FUNCTION_INFO_V1(pcpatch_uncompress);
Datum pcpatch_uncompress(PG_FUNCTION_ARGS)
{
	SERIALIZED_PATCH *serpa = PG_GETARG_SERPATCH_P(0);
	PCSCHEMA *schema = pc_schema_from_pcid(serpa->pcid, fcinfo);
	PCPATCH *patch = pc_patch_deserialize(serpa, schema);
	SERIALIZED_PATCH *serpa_out = pc_patch_serialize_to_uncompressed(patch);
	pc_patch_free(patch);
	PG_RETURN_POINTER(serpa_out);
}

PG_FUNCTION_INFO_V1(pcpatch_compress);
Datum pcpatch_compress(PG_FUNCTION_ARGS)
{
	SERIALIZED_PATCH *serpa = PG_GETARG_SERPATCH_P(0);
	text *compr_in_text = PG_GETARG_TEXT_P(1);
	char *compr_in = text_to_cstring(compr_in_text);
	text *config_in_text = PG_GETARG_TEXT_P(2);
	char *config_in = text_to_cstring(config_in_text);
	PCSCHEMA *schema = pc_schema_from_pcid(serpa->pcid, fcinfo);
	PCPATCH *patch_in = pc_patch_deserialize(serpa, schema);
	PCPATCH *pa = patch_in;
	SERIALIZED_PATCH *serpa_out;
	PCDIMSTATS *stats = NULL;
	int i;

	/* Uncompress first */
	if ( patch_in->type != PC_NONE ) {
	pa = pc_patch_uncompress(patch_in);
	}

	schema = pc_schema_clone(schema); /* we're going to modify it */

	/* Set compression scheme */
	if ( *compr_in == '\0' || strcasecmp(compr_in, "auto") == 0 ) {
		/* keep schema defined compression */
	}
	else if ( strcmp(compr_in, "dimensional") == 0 ) {{
		char *ptr = config_in;
		PCPATCH_DIMENSIONAL *pdl = pc_patch_dimensional_from_uncompressed((PCPATCH_UNCOMPRESSED*)pa);
		schema->compression = PC_DIMENSIONAL;
		stats = pc_dimstats_make(schema);
		pc_dimstats_update(stats, pdl);
		/* make sure to avoid stat updates (not sure if needed) */
		stats->total_points = PCDIMSTATS_MIN_SAMPLE+1;


		/* Fill in per-dimension compression */
		if ( *ptr )
		for (i=0; i<stats->ndims; ++i) {
			PCDIMSTAT *stat = &(stats->stats[i]);
			/*pcinfo("ptr: %s", ptr);*/
			if ( *ptr == ',' || strncmp(ptr, "auto", strlen("auto")) == 0 ) {
				/* leave auto-determined compression */
			}
			else if ( strncmp(ptr, "rle", strlen("rle")) == 0 ) {
				stat->recommended_compression = PC_DIM_RLE;
			}
			else if ( strncmp(ptr, "sigbits", strlen("sigbits")) == 0 ) {
				stat->recommended_compression = PC_DIM_SIGBITS;
			}
			else if ( strncmp(ptr, "zlib", strlen("zlib")) == 0 ) {
				stat->recommended_compression = PC_DIM_ZLIB;
			}
			else {
				elog(ERROR, "Unrecognized dimensional compression '%s'. Please specify 'auto', 'rle', 'sigbits' or 'zlib'", ptr);
			}
			while (*ptr && *ptr != ',') ++ptr;
			if ( ! *ptr ) break;
			else ++ptr;
		}

		if ( pa != patch_in ) pc_patch_free(pa);
		pa = (PCPATCH*)pc_patch_dimensional_compress(pdl, stats);
		pc_patch_dimensional_free(pdl);
	}}
	else if ( strcmp(compr_in, "laz") == 0 ) {
		schema->compression = PC_LAZPERF;
	}
	else {
		elog(ERROR, "Unrecognized compression '%s'. Please specify 'auto','dimensional' or 'laz'", compr_in);
	}

	pa->schema = schema; /* install overridden schema */
	serpa_out = pc_patch_serialize(pa, stats);

	if ( pa != patch_in )
		pc_patch_free(pa);
	pc_patch_free(patch_in);
	pc_schema_free(schema);

	PG_RETURN_POINTER(serpa_out);
}

PG_FUNCTION_INFO_V1(pcpatch_numpoints);
Datum pcpatch_numpoints(PG_FUNCTION_ARGS)
{
	SERIALIZED_PATCH *serpa = PG_GETHEADER_SERPATCH_P(0);
	PG_RETURN_INT32(serpa->npoints);
}

PG_FUNCTION_INFO_V1(pcpatch_pointn);
Datum pcpatch_pointn(PG_FUNCTION_ARGS)
{
	SERIALIZED_POINT *serpt;
	SERIALIZED_PATCH *serpa = PG_GETARG_SERPATCH_P(0);
	int32 n = PG_GETARG_INT32(1);
	PCSCHEMA *schema = pc_schema_from_pcid(serpa->pcid, fcinfo);
	PCPATCH *patch = pc_patch_deserialize(serpa, schema);
	PCPOINT *pt = NULL;
	if(patch) {
		pt = pc_patch_pointn(patch,n);
		pc_patch_free(patch);
	}
	if(!pt) PG_RETURN_NULL();
	serpt = pc_point_serialize(pt);
	pc_point_free(pt);
	PG_RETURN_POINTER(serpt);
}

PG_FUNCTION_INFO_V1(pcpatch_range);
Datum pcpatch_range(PG_FUNCTION_ARGS)
{
	SERIALIZED_PATCH *serpaout;
	SERIALIZED_PATCH *serpa = PG_GETARG_SERPATCH_P(0);
	int32 first = PG_GETARG_INT32(1);
	int32 count = PG_GETARG_INT32(2);
	PCSCHEMA *schema = pc_schema_from_pcid(serpa->pcid, fcinfo);
	PCPATCH *patch = pc_patch_deserialize(serpa, schema);
	PCPATCH *patchout = NULL;
	if ( patch )
	{
		patchout = pc_patch_range(patch, first, count);
		if ( patchout != patch )
			pc_patch_free(patch);
	}
	if ( !patchout )
		PG_RETURN_NULL();
	serpaout = pc_patch_serialize(patchout, NULL);
	pc_patch_free(patchout);
	PG_RETURN_POINTER(serpaout);
}

PG_FUNCTION_INFO_V1(pcpatch_pcid);
Datum pcpatch_pcid(PG_FUNCTION_ARGS)
{
	SERIALIZED_PATCH *serpa = PG_GETHEADER_SERPATCH_P(0);
	PG_RETURN_INT32(serpa->pcid);
}

PG_FUNCTION_INFO_V1(pcpatch_summary);
Datum pcpatch_summary(PG_FUNCTION_ARGS)
{
	const int stats_size_guess = 400;
	SERIALIZED_PATCH *serpa;
	PCSCHEMA *schema;
	PCSTATS *stats;
	PCPATCH *patch = NULL;
	StringInfoData strdata;
	text *ret;
	const char *comma = "";
	int i;

	serpa = PG_GETHEADERX_SERPATCH_P(0, stats_size_guess);
	schema = pc_schema_from_pcid(serpa->pcid, fcinfo);
	if ( serpa->compression == PC_DIMENSIONAL )
	{
		/* need full data to inspect per-dimension compression */
		/* NOTE: memory usage could be optimized to only fetch slices
		 *       at specific offsets, but doesn't seem worth at this time
		 *       See https://github.com/pgpointcloud/pointcloud/pull/51#issuecomment-83592363
		 */
		serpa = PG_GETARG_SERPATCH_P(0);
		patch = pc_patch_deserialize(serpa, schema);
	}
	else if ( stats_size_guess < pc_stats_size(schema) )
	{
		/* only need stats here */
		serpa = PG_GETHEADERX_SERPATCH_P(0, pc_stats_size(schema));
	}
	stats = pc_patch_stats_deserialize(schema, serpa->data);

	initStringInfo(&strdata);
	/* Make space for VARSIZ, see SET_VARSIZE below */
	appendStringInfoSpaces(&strdata, VARHDRSZ);

	appendStringInfo(&strdata, "{"
		"\"pcid\":%d, \"npts\":%d, \"srid\":%d, "
		"\"compr\":\"%s\",\"dims\":[",
		serpa->pcid, serpa->npoints, schema->srid,
		pc_compression_name(serpa->compression));

	for (i=0; i<schema->ndims; ++i)
	{
		PCDIMENSION *dim = schema->dims[i];
		PCBYTES bytes;
		double val;
		appendStringInfo(&strdata,
			"%s{\"pos\":%d,\"name\":\"%s\",\"size\":%d"
			",\"type\":\"%s\"",
			comma, dim->position, dim->name, dim->size,
			pc_interpretation_string(dim->interpretation));

		/* Print per-dimension compression (if dimensional) */
		if ( serpa->compression == PC_DIMENSIONAL )
		{
			bytes = ((PCPATCH_DIMENSIONAL*)patch)->bytes[i];
			switch ( bytes.compression )
			{
			case PC_DIM_RLE:
				appendStringInfoString(&strdata,",\"compr\":\"rle\"");
				break;
			case PC_DIM_SIGBITS:
				appendStringInfoString(&strdata,",\"compr\":\"sigbits\"");
				break;
			case PC_DIM_ZLIB:
				appendStringInfoString(&strdata,",\"compr\":\"zlib\"");
				break;
			case PC_DIM_NONE:
				appendStringInfoString(&strdata,",\"compr\":\"none\"");
				break;
			default:
				appendStringInfo(&strdata,",\"compr\":\"unknown(%d)\"",
				bytes.compression);
				break;
			}
		}

		if ( stats )
		{
			pc_point_get_double_by_name(&(stats->min), dim->name, &val);
			appendStringInfo(&strdata,",\"stats\":{\"min\":%g", val);
			pc_point_get_double_by_name(&(stats->max), dim->name, &val);
			appendStringInfo(&strdata,",\"max\":%g", val);
			pc_point_get_double_by_name(&(stats->avg), dim->name, &val);
			appendStringInfo(&strdata,",\"avg\":%g}", val);
		}
		appendStringInfoString(&strdata, "}");
		comma = ",";
	}

	appendStringInfoString(&strdata, "]}");

	ret = (text*)strdata.data;
	SET_VARSIZE(ret, strdata.len);
	PG_RETURN_TEXT_P(ret);
}

PG_FUNCTION_INFO_V1(pcpatch_compression);
Datum pcpatch_compression(PG_FUNCTION_ARGS)
{
	SERIALIZED_PATCH *serpa = PG_GETHEADER_SERPATCH_P(0);
	PG_RETURN_INT32(serpa->compression);
}

PG_FUNCTION_INFO_V1(pcpatch_intersects);
Datum pcpatch_intersects(PG_FUNCTION_ARGS)
{
	SERIALIZED_PATCH *serpa1 = PG_GETHEADER_SERPATCH_P(0);
	SERIALIZED_PATCH *serpa2 = PG_GETHEADER_SERPATCH_P(1);

	if ( serpa1->pcid != serpa2->pcid )
		elog(ERROR, "%s: pcid mismatch (%d != %d)", __func__, serpa1->pcid, serpa2->pcid);

	if ( pc_bounds_intersects(&(serpa1->bounds), &(serpa2->bounds)) )
	{
		PG_RETURN_BOOL(true);
	}
	PG_RETURN_BOOL(false);
}

PG_FUNCTION_INFO_V1(pcpatch_size);
Datum pcpatch_size(PG_FUNCTION_ARGS)
{
	SERIALIZED_PATCH *serpa = PG_GETARG_SERPATCH_P(0);
	PG_RETURN_INT32(VARSIZE(serpa));
}

PG_FUNCTION_INFO_V1(pcpoint_size);
Datum pcpoint_size(PG_FUNCTION_ARGS)
{
	SERIALIZED_POINT *serpt = PG_GETARG_SERPOINT_P(0);
	PG_RETURN_INT32(VARSIZE(serpt));
}

PG_FUNCTION_INFO_V1(pcpoint_pcid);
Datum pcpoint_pcid(PG_FUNCTION_ARGS)
{
	SERIALIZED_POINT *serpt = PG_GETARG_SERPOINT_P(0);
	PG_RETURN_INT32(serpt->pcid);
}

PG_FUNCTION_INFO_V1(pc_version);
Datum pc_version(PG_FUNCTION_ARGS)
{
	text *version_text;
	char version[64];
	snprintf(version, 64, "%s", POINTCLOUD_VERSION);
	version_text = cstring_to_text(version);
	PG_RETURN_TEXT_P(version_text);
}

PG_FUNCTION_INFO_V1(pc_pgsql_version);
Datum pc_pgsql_version(PG_FUNCTION_ARGS)
{
	text *version_text;
	char version[12];
	snprintf(version, 12, "%d", PGSQL_VERSION);
	version_text = cstring_to_text(version);
	PG_RETURN_TEXT_P(version_text);
}

PG_FUNCTION_INFO_V1(pc_libxml2_version);
Datum pc_libxml2_version(PG_FUNCTION_ARGS)
{
	text *version_text;
	char version[64];
	snprintf(version, 64, "%s", LIBXML2_VERSION);
	version_text = cstring_to_text(version);
	PG_RETURN_TEXT_P(version_text);
}

PG_FUNCTION_INFO_V1(pc_lazperf_enabled);
Datum pc_lazperf_enabled(PG_FUNCTION_ARGS)
{
#ifdef HAVE_LAZPERF
	PG_RETURN_BOOL(true);
#else
	PG_RETURN_BOOL(false);
#endif
}

/**
* Read a named dimension statistic from a PCPATCH
* PC_PatchMax(patch pcpatch, dimname text) returns Numeric
* PC_PatchMin(patch pcpatch, dimname text) returns Numeric
* PC_PatchAvg(patch pcpatch, dimname text) returns Numeric
* PC_PatchMax(patch pcpatch) returns PcPoint
* PC_PatchMin(patch pcpatch) returns PcPoint
* PC_PatchAvg(patch pcpatch) returns PcPoint
*/
PG_FUNCTION_INFO_V1(pcpatch_get_stat);
Datum pcpatch_get_stat(PG_FUNCTION_ARGS)
{
	static int stats_size_guess = 400;
	SERIALIZED_PATCH *serpa = PG_GETHEADERX_SERPATCH_P(0, stats_size_guess);
	PCSCHEMA *schema = pc_schema_from_pcid(serpa->pcid, fcinfo);
	int32 statno = PG_GETARG_INT32(1);
	char *dim_str = 0;
	PCSTATS *stats;
	const PCPOINT *pt;
	SERIALIZED_POINT *serpt = NULL;
	float8 double_result;
	int rv = 1;


	if ( PG_NARGS() > 2 ) {
		/* TODO: only get small slice ? */
		dim_str = text_to_cstring(PG_GETARG_TEXT_P(2));
	}

	if ( stats_size_guess < pc_stats_size(schema) )
	{
		serpa = PG_GETHEADERX_SERPATCH_P(0, pc_stats_size(schema) );
	}

	stats = pc_patch_stats_deserialize(schema, serpa->data);

	if ( ! stats )
		PG_RETURN_NULL();

	/* Min */
	if ( 0 == statno )
		pt = &(stats->min);
	/* Max */
	else if ( 1 == statno )
		pt = &(stats->max);
	/* Avg */
	else if ( 2 == statno )
		pt = &(stats->avg);
	/* Unsupported */
	else
		elog(ERROR, "stat number \"%d\" is not supported", statno);

	/* empty dim string means we want the whole point */
	if ( ! dim_str )
	{
		serpt = pc_point_serialize(pt);
		pc_stats_free(stats);
		PG_RETURN_POINTER(serpt);
	}
	else
	{
		rv = pc_point_get_double_by_name(pt, dim_str, &double_result);
		pc_stats_free(stats);
		if ( ! rv )
		{
			elog(ERROR, "dimension \"%s\" does not exist in schema", dim_str);
			PG_RETURN_NULL();
		}
		pfree(dim_str);
		PG_RETURN_DATUM(DirectFunctionCall1(float8_numeric, Float8GetDatum(double_result)));
	}
}

/**
* PC_FilterLessThan(patch pcpatch, dimname text, value) returns PcPatch
* PC_FilterGreaterThan(patch pcpatch, dimname text, value) returns PcPatch
* PC_FilterEquals(patch pcpatch, dimname text, value) returns PcPatch
* PC_FilterBetween(patch pcpatch, dimname text, value1, value2) returns PcPatch
*/
PG_FUNCTION_INFO_V1(pcpatch_filter);
Datum pcpatch_filter(PG_FUNCTION_ARGS)
{
	SERIALIZED_PATCH *serpatch = PG_GETARG_SERPATCH_P(0);
	PCSCHEMA *schema = pc_schema_from_pcid(serpatch->pcid, fcinfo);
	char *dim_name = text_to_cstring(PG_GETARG_TEXT_P(1));
	float8 value1 = PG_GETARG_FLOAT8(2);
	float8 value2 = PG_GETARG_FLOAT8(3);
	int32 mode = PG_GETARG_INT32(4);
	PCPATCH *patch;
	PCPATCH *patch_filtered = NULL;
	SERIALIZED_PATCH *serpatch_filtered;

	patch = pc_patch_deserialize(serpatch, schema);
	if ( ! patch )
	{
		elog(ERROR, "failed to deserialize patch");
		PG_RETURN_NULL();
	}

	switch ( mode )
	{
	case 0:
		patch_filtered = pc_patch_filter_lt_by_name(patch, dim_name, value1);
		break;
	case 1:
		patch_filtered = pc_patch_filter_gt_by_name(patch, dim_name, value1);
		break;
	case 2:
		patch_filtered = pc_patch_filter_equal_by_name(patch, dim_name, value1);
		break;
	case 3:
		patch_filtered = pc_patch_filter_between_by_name(patch, dim_name, value1, value2);
		break;
	default:
		elog(ERROR, "unknown mode \"%d\"", mode);
	}

	pc_patch_free(patch);
	PG_FREE_IF_COPY(serpatch, 0);

	if ( ! patch_filtered )
	{
		elog(ERROR, "dimension \"%s\" does not exist", dim_name);
	}
	pfree(dim_name);

	/* Always treat zero-point patches as SQL NULL */
	if ( patch_filtered->npoints <= 0 )
	{
		pc_patch_free(patch_filtered);
		PG_RETURN_NULL();
	}

	serpatch_filtered = pc_patch_serialize(patch_filtered, NULL);
	pc_patch_free(patch_filtered);

	PG_RETURN_POINTER(serpatch_filtered);
}

const char **array_to_cstring_array(ArrayType *array, int *size)
{
	int i, j, offset = 0;
	int nelems = ArrayGetNItems(ARR_NDIM(array), ARR_DIMS(array));
	const char **cstring = nelems ? pcalloc(nelems * sizeof(char*)) : NULL;
	bits8 *bitmap = ARR_NULLBITMAP(array);
	for(i=j=0; i<nelems; ++i)
	{
		text *array_text;
		if(array_get_isnull(bitmap,i)) continue;

		array_text = (text *)(ARR_DATA_PTR(array)+offset);
		cstring[j++] = text_to_cstring(array_text);
		offset += INTALIGN(VARSIZE(array_text));
	}
	if(size) *size = j;
	return cstring;
}

void pc_cstring_array_free(const char **array, int nelems)
{
	int i;
	if(!array) return;
	for(i=0;i<nelems;++i) pfree((void *)array[i]);
	pcfree((void *)array);
}

/**
* PC_Sort(patch pcpatch, dimname text[]) returns PcPatch
*/
PG_FUNCTION_INFO_V1(pcpatch_sort);
Datum pcpatch_sort(PG_FUNCTION_ARGS)
{
	SERIALIZED_PATCH *serpatch = PG_GETARG_SERPATCH_P(0);
	ArrayType *array = PG_GETARG_ARRAYTYPE_P(1);
	PCSCHEMA *schema = NULL;
	PCPATCH *patch = NULL;
	PCPATCH *patch_sorted = NULL;
	SERIALIZED_PATCH *serpatch_sorted = NULL;

	int ndims;
	const char **dim_name = array_to_cstring_array(array,&ndims);
	if(!ndims)
	{
		pc_cstring_array_free(dim_name,ndims);
		PG_RETURN_POINTER(serpatch);
	}

	schema = pc_schema_from_pcid(serpatch->pcid, fcinfo);

	patch = pc_patch_deserialize(serpatch, schema);
	if(patch) patch_sorted = pc_patch_sort(patch,dim_name,ndims);

	pc_cstring_array_free(dim_name,ndims);
	if(patch) pc_patch_free(patch);
	PG_FREE_IF_COPY(serpatch, 0);

	if(!patch_sorted) PG_RETURN_NULL();

	serpatch_sorted = pc_patch_serialize(patch_sorted,NULL);
	pc_patch_free(patch_sorted);
	PG_RETURN_POINTER(serpatch_sorted);
}

/** True/false if the patch is sorted on dimension */
PG_FUNCTION_INFO_V1(pcpatch_is_sorted);
Datum pcpatch_is_sorted(PG_FUNCTION_ARGS)
{
	ArrayType *array = PG_GETARG_ARRAYTYPE_P(1);
	bool strict = PG_GETARG_BOOL(2);
	PCSCHEMA *schema = NULL;
	SERIALIZED_PATCH *serpatch = NULL;
	PCPATCH *patch = NULL;
	int ndims;
	uint32_t res;
	const char **dim_name = array_to_cstring_array(array,&ndims);
	if(!ndims)
	{
		pc_cstring_array_free(dim_name,ndims);
		PG_RETURN_BOOL(PC_TRUE);
	}
	serpatch = PG_GETARG_SERPATCH_P(0);
	schema = pc_schema_from_pcid(serpatch->pcid, fcinfo);
	patch = pc_patch_deserialize(serpatch, schema);

	res = pc_patch_is_sorted(patch,dim_name,ndims,strict);

	pc_cstring_array_free(dim_name,ndims);
	pc_patch_free(patch);

	if(res == PC_FAILURE-1)
		elog(ERROR, "PC_IsSorted failed");

	PG_RETURN_BOOL(res);
}

