/***********************************************************************
 * pc_pgsql.c
 *
 *  Utility functions to bind pc_api.h functions to PgSQL, including
 *  memory management and serialization/deserializations.
 *
 *  PgSQL Pointcloud is free and open source software provided
 *  by the Government of Canada
 *  Copyright (c) 2013 Natural Resources Canada
 *
 ***********************************************************************/

#include "pc_pgsql.h"

#include "access/hash.h"
#include "access/heapam.h"
#include "access/htup_details.h"
#include "access/sysattr.h"

#include "catalog/indexing.h"
#include "catalog/namespace.h"
#include "catalog/pg_extension.h"

#include "commands/extension.h"

#include "executor/spi.h"

#include "utils/fmgroids.h"
#include "utils/hsearch.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/regproc.h"

#include <assert.h>

PG_MODULE_MAGIC;

/**********************************************************************************
 * Pointcloud constants cache (mainly based on PostGIS source code)
 */

static PC_CONSTANTS *pc_constants_cache = NULL;

static Oid pointcloud_get_full_version_schema()
{
  const char *proname = "pointcloud_full_version";
  List *names = stringToQualifiedNameList(proname);
#if PGSQL_VERSION < 140
  FuncCandidateList clist =
      FuncnameGetCandidates(names, -1, NIL, false, false, false);
#else
  FuncCandidateList clist =
      FuncnameGetCandidates(names, -1, NIL, false, false, false, false);
#endif
  if (!clist)
    return InvalidOid;

  return get_func_namespace(clist->oid);
}

static Oid pointcloud_get_extension_schema(Oid ext_oid)
{
  Oid result;
  SysScanDesc scandesc;
  HeapTuple tuple;
  ScanKeyData entry[1];

#if PGSQL_VERSION < 120
  Relation rel = heap_open(ExtensionRelationId, AccessShareLock);
  ScanKeyInit(&entry[0], ObjectIdAttributeNumber, BTEqualStrategyNumber,
              F_OIDEQ, ObjectIdGetDatum(ext_oid));
#else
  Relation rel = table_open(ExtensionRelationId, AccessShareLock);
  ScanKeyInit(&entry[0], Anum_pg_extension_oid, BTEqualStrategyNumber, F_OIDEQ,
              ObjectIdGetDatum(ext_oid));
#endif

  scandesc = systable_beginscan(rel, ExtensionOidIndexId, true, NULL, 1, entry);

  tuple = systable_getnext(scandesc);

  /* We assume that there can be at most one matching tuple */
  if (HeapTupleIsValid(tuple))
    result = ((Form_pg_extension)GETSTRUCT(tuple))->extnamespace;
  else
    result = InvalidOid;

  systable_endscan(scandesc);

#if PGSQL_VERSION < 120
  heap_close(rel, AccessShareLock);
#else
  table_close(rel, AccessShareLock);
#endif

  return result;
}

void pointcloud_init_constants_cache(void)
{
  char *nsp_name;
  Oid ext_oid;
  Oid nsp_oid = InvalidOid;
  MemoryContext context;

  if (pc_constants_cache)
    return;

  ext_oid = get_extension_oid("pointcloud", true);
  if (ext_oid != InvalidOid)
  {
    nsp_oid = pointcloud_get_extension_schema(ext_oid);
  }
  else
  {
    nsp_oid = pointcloud_get_full_version_schema();
  }

  /* early exit if we cannot lookup nsp_name */
  if (nsp_oid == InvalidOid)
    elog(ERROR, "Unable to determine 'pointcloud' install schema");

  nsp_name = get_namespace_name(nsp_oid);

  /* Put constants cache in a child of the CacheContext */
  context = AllocSetContextCreate(
      CacheMemoryContext, "Pointcloud Constants Context", ALLOCSET_SMALL_SIZES);

  /* Allocate in the CacheContext so we don't lose this at the end of the
   * statement */
  pc_constants_cache = MemoryContextAlloc(context, sizeof(PC_CONSTANTS));

  pc_constants_cache->schema =
      MemoryContextStrdup(CacheMemoryContext, nsp_name);

  pc_constants_cache->formats =
      MemoryContextStrdup(CacheMemoryContext, "pointcloud_formats");
  pc_constants_cache->formats_srid =
      MemoryContextStrdup(CacheMemoryContext, "srid");
  pc_constants_cache->formats_schema =
      MemoryContextStrdup(CacheMemoryContext, "schema");
}

/**********************************************************************************
 * POSTGRESQL MEMORY MANAGEMENT HOOKS
 */

static void *pgsql_alloc(size_t size)
{
  void *result;
  result = palloc(size);

  if (!result)
  {
    ereport(ERROR,
            (errcode(ERRCODE_OUT_OF_MEMORY), errmsg("Out of virtual memory")));
  }

  return result;
}

static void *pgsql_realloc(void *mem, size_t size)
{
  void *result;
  result = repalloc(mem, size);
  if (!result)
  {
    ereport(ERROR,
            (errcode(ERRCODE_OUT_OF_MEMORY), errmsg("Out of virtual memory")));
  }
  return result;
}

static void pgsql_free(void *ptr) { pfree(ptr); }

static void pgsql_msg_handler(int sig, const char *fmt, va_list ap)
    __attribute__((format(printf, 2, 0)));

static void pgsql_msg_handler(int sig, const char *fmt, va_list ap)
{
#define MSG_MAXLEN 1024
  char msg[MSG_MAXLEN] = {0};
  vsnprintf(msg, MSG_MAXLEN, fmt, ap);
  msg[MSG_MAXLEN - 1] = '\0';
  ereport(sig, (errmsg_internal("%s", msg)));
}

static void pgsql_error(const char *fmt, va_list ap)
    __attribute__((format(printf, 1, 0)));

static void pgsql_error(const char *fmt, va_list ap)
{
  pgsql_msg_handler(ERROR, fmt, ap);
}

static void pgsql_warn(const char *fmt, va_list ap)
    __attribute__((format(printf, 1, 0)));

static void pgsql_warn(const char *fmt, va_list ap)
{
  pgsql_msg_handler(WARNING, fmt, ap);
}

static void pgsql_info(const char *fmt, va_list ap)
    __attribute__((format(printf, 1, 0)));

static void pgsql_info(const char *fmt, va_list ap)
{
  pgsql_msg_handler(NOTICE, fmt, ap);
}

/**********************************************************************************
 * POINTCLOUD START-UP/SHUT-DOWN CALLBACKS
 */

/**
 * On module load we want to hook the message writing and memory allocation
 * functions of libpc to the PostgreSQL ones.
 * TODO: also hook the libxml2 hooks into PostgreSQL.
 */
void _PG_init(void);
void _PG_init(void)
{
  elog(LOG, "Pointcloud (%s) module loaded", POINTCLOUD_VERSION);
  pc_set_handlers(pgsql_alloc, pgsql_realloc, pgsql_free, pgsql_error,
                  pgsql_info, pgsql_warn);
}

/* Module unload callback */
void _PG_fini(void);
void _PG_fini(void)
{
  elog(LOG, "Pointcloud (%s) module unloaded", POINTCLOUD_VERSION);
}

/* Mask pcid from bottom of typmod */
uint32 pcid_from_typmod(const int32 typmod)
{
  if (typmod == -1)
    return 0;
  else
    return (typmod & 0x0000FFFF);
}

/**********************************************************************************
 * PCPOINT WKB Handling
 */

PCPOINT *
#if PGSQL_VERSION < 120
pc_point_from_hexwkb(const char *hexwkb, size_t hexlen,
                     FunctionCallInfoData *fcinfo)
#else
pc_point_from_hexwkb(const char *hexwkb, size_t hexlen, FunctionCallInfo fcinfo)
#endif
{
  PCPOINT *pt;
  PCSCHEMA *schema;
  uint32 pcid;
  uint8 *wkb = pc_bytes_from_hexbytes(hexwkb, hexlen);
  size_t wkblen = hexlen / 2;
  pcid = pc_wkb_get_pcid(wkb);
  schema = pc_schema_from_pcid(pcid, fcinfo);
  pt = pc_point_from_wkb(schema, wkb, wkblen);
  pfree(wkb);
  return pt;
}

char *pc_point_to_hexwkb(const PCPOINT *pt)
{
  uint8 *wkb;
  size_t wkb_size;
  char *hexwkb;

  wkb = pc_point_to_wkb(pt, &wkb_size);
  hexwkb = pc_hexbytes_from_bytes(wkb, wkb_size);
  pfree(wkb);
  return hexwkb;
}

/**********************************************************************************
 * PCPATCH WKB Handling
 */

PCPATCH *
#if PGSQL_VERSION < 120
pc_patch_from_hexwkb(const char *hexwkb, size_t hexlen,
                     FunctionCallInfoData *fcinfo)
#else
pc_patch_from_hexwkb(const char *hexwkb, size_t hexlen, FunctionCallInfo fcinfo)
#endif
{
  PCPATCH *patch;
  PCSCHEMA *schema;
  uint32 pcid;
  uint8 *wkb = pc_bytes_from_hexbytes(hexwkb, hexlen);
  size_t wkblen = hexlen / 2;
  pcid = pc_wkb_get_pcid(wkb);
  if (!pcid)
    elog(ERROR, "%s: pcid is zero", __func__);

  schema = pc_schema_from_pcid(pcid, fcinfo);
  if (!schema)
    elog(ERROR, "%s: unable to look up schema entry", __func__);

  patch = pc_patch_from_wkb(schema, wkb, wkblen);
  pfree(wkb);
  return patch;
}

char *pc_patch_to_hexwkb(const PCPATCH *patch)
{
  uint8 *wkb;
  size_t wkb_size;
  char *hexwkb;

  wkb = pc_patch_to_wkb(patch, &wkb_size);
  hexwkb = pc_hexbytes_from_bytes(wkb, wkb_size);
  pfree(wkb);
  return hexwkb;
}

/**********************************************************************************
 * PCID <=> PCSCHEMA translation via POINTCLOUD_FORMATS
 */

uint32 pcid_from_datum(Datum d)
{
  SERIALIZED_POINT *serpart;
  if (!d)
    return 0;
  /* Serializations are int32_t <size> uint32_t <pcid> == 8 bytes */
  /* Cast to SERIALIZED_POINT for convenience, SERIALIZED_PATCH shares same
   * header */
  serpart = (SERIALIZED_POINT *)PG_DETOAST_DATUM_SLICE(d, 0, 8);
  return serpart->pcid;
}

PCSCHEMA *pc_schema_from_pcid_uncached(uint32 pcid)
{
  char sql[256];
  char *xml, *xml_spi, *srid_spi, *formats;
  int err, srid;
  size_t size;
  PCSCHEMA *schema;

  if (SPI_OK_CONNECT != SPI_connect())
  {
    elog(ERROR, "%s: could not connect to SPI manager", __func__);
    return NULL;
  }

  if (!pc_constants_cache)
  {
    elog(ERROR, "%s: constants cache is not initialized", __func__);
    return NULL;
  }

  formats = quote_qualified_identifier(pc_constants_cache->schema,
                                       pc_constants_cache->formats);
  sprintf(sql, "select %s, %s from %s where pcid = %d",
          pc_constants_cache->formats_schema, pc_constants_cache->formats_srid,
          formats, pcid);
  err = SPI_exec(sql, 1);

  if (err < 0)
  {
    elog(ERROR, "%s: error (%d) executing query: %s", __func__, err, sql);
    return NULL;
  }

  /* No entry in POINTCLOUD_FORMATS */
  if (SPI_processed <= 0)
  {
    elog(ERROR, "no entry in \"%s\" for pcid = %d", formats, pcid);
    return NULL;
  }

  /* Result  */
  xml_spi = SPI_getvalue(SPI_tuptable->vals[0], SPI_tuptable->tupdesc, 1);
  srid_spi = SPI_getvalue(SPI_tuptable->vals[0], SPI_tuptable->tupdesc, 2);

  /* NULL result */
  if (!(xml_spi && srid_spi))
  {
    elog(ERROR, "unable to read row from \"%s\" for pcid = %d", formats, pcid);
    return NULL;
  }

  /* Copy result to upper executor context */
  size = strlen(xml_spi) + 1;
  xml = SPI_palloc(size);
  memcpy(xml, xml_spi, size);

  /* Parse the SRID string into the function stack */
  srid = atoi(srid_spi);

  /* Disconnect from SPI, losing all our SPI-allocated memory now... */
  SPI_finish();

  /* Build the schema object */
  schema = pc_schema_from_xml(xml);

  if (!schema)
  {
    ereport(ERROR, (errcode(ERRCODE_NOT_AN_XML_DOCUMENT),
                    errmsg("unable to parse XML for pcid = %d in \"%s\"", pcid,
                           formats)));
  }

  schema->pcid = pcid;
  schema->srid = srid;

  return schema;
}

/**
 * Hold the schema references in a list.
 * We'll just search them linearly, because
 * usually we'll have only one per statement
 */
#define SchemaCacheSize 16

typedef struct
{
  int next_slot;
  int pcids[SchemaCacheSize];
  PCSCHEMA *schemas[SchemaCacheSize];
} SchemaCache;

/**
 * Get the schema entry from the schema cache if one exists.
 * If it doesn't exist, make a new empty one, cache it, and
 * return it.
 */
static SchemaCache *
#if PGSQL_VERSION < 120
GetSchemaCache(FunctionCallInfoData *fcinfo)
#else
GetSchemaCache(FunctionCallInfo fcinfo)
#endif
{
  SchemaCache *cache = fcinfo->flinfo->fn_extra;
  if (!cache)
  {
    cache = MemoryContextAlloc(fcinfo->flinfo->fn_mcxt, sizeof(SchemaCache));
    memset(cache, 0, sizeof(SchemaCache));
    fcinfo->flinfo->fn_extra = cache;
  }
  return cache;
}

PCSCHEMA *
#if PGSQL_VERSION < 120
pc_schema_from_pcid(uint32 pcid, FunctionCallInfoData *fcinfo)
#else
pc_schema_from_pcid(uint32 pcid, FunctionCallInfo fcinfo)
#endif
{
  SchemaCache *schema_cache = GetSchemaCache(fcinfo);
  int i;
  PCSCHEMA *schema;
  MemoryContext oldcontext;

  /* Unable to find/make a schema cache? Odd. */
  if (!schema_cache)
  {
    ereport(ERROR,
            (errcode(ERRCODE_INTERNAL_ERROR),
             errmsg("unable to create/load statement level schema cache")));
  }

  /* Find our PCID if it's in there (usually it will be first) */
  for (i = 0; i < SchemaCacheSize; i++)
  {
    if (schema_cache->pcids[i] == pcid)
    {
      return schema_cache->schemas[i];
    }
  }

  elog(DEBUG1, "schema cache miss, use pc_schema_from_pcid_uncached");

  /* Not in there, load one the old-fashioned way. */
  oldcontext = MemoryContextSwitchTo(fcinfo->flinfo->fn_mcxt);
  pointcloud_init_constants_cache();
  schema = pc_schema_from_pcid_uncached(pcid);
  MemoryContextSwitchTo(oldcontext);

  /* Failed to load the XML? Odd. */
  if (!schema)
  {
    ereport(ERROR, (errcode(ERRCODE_INTERNAL_ERROR),
                    errmsg("unable to load schema for pcid %u", pcid)));
  }

  /* Save the XML in the next unused slot */
  schema_cache->schemas[schema_cache->next_slot] = schema;
  schema_cache->pcids[schema_cache->next_slot] = pcid;
  schema_cache->next_slot = (schema_cache->next_slot + 1) % SchemaCacheSize;
  return schema;
}

/**********************************************************************************
 * SERIALIZATION/DESERIALIZATION UTILITIES
 */

SERIALIZED_POINT *pc_point_serialize(const PCPOINT *pcpt)
{
  size_t serpt_size = sizeof(SERIALIZED_POINT) - 1 + pcpt->schema->size;
  SERIALIZED_POINT *serpt = palloc(serpt_size);
  serpt->pcid = pcpt->schema->pcid;
  memcpy(serpt->data, pcpt->data, pcpt->schema->size);
  SET_VARSIZE(serpt, serpt_size);
  return serpt;
}

PCPOINT *pc_point_deserialize(const SERIALIZED_POINT *serpt,
                              const PCSCHEMA *schema)
{
  PCPOINT *pcpt;
  size_t pgsize = VARSIZE(serpt) + 1 - sizeof(SERIALIZED_POINT);
  /*
   * Big problem, the size on disk doesn't match what we expect,
   * so we cannot reliably interpret the contents.
   */
  if (schema->size != pgsize)
  {
    elog(ERROR, "schema size and disk size mismatch, repair the schema");
    return NULL;
  }
  pcpt = pc_point_from_data(schema, serpt->data);
  return pcpt;
}

size_t pc_patch_serialized_size(const PCPATCH *patch)
{
  size_t stats_size = pc_stats_size(patch->schema);
  size_t common_size = BUFFERALIGN(sizeof(SERIALIZED_PATCH)) - 1;
  switch (patch->type)
  {
  case PC_NONE:
  {
    PCPATCH_UNCOMPRESSED *pu = (PCPATCH_UNCOMPRESSED *)patch;
    return common_size + stats_size + pu->datasize;
  }
  case PC_DIMENSIONAL:
  {
    return common_size + stats_size +
           pc_patch_dimensional_serialized_size((PCPATCH_DIMENSIONAL *)patch);
  }
  case PC_LAZPERF:
  {
    static size_t lazsize_size = 4;
    PCPATCH_LAZPERF *pg = (PCPATCH_LAZPERF *)patch;
    return common_size + stats_size + lazsize_size + pg->lazperfsize;
  }
  default:
  {
    pcerror("%s: unknown compresed %d", __func__, patch->type);
  }
  }
  return -1;
}

static size_t pc_patch_stats_serialize(uint8_t *buf, const PCSCHEMA *schema,
                                       const PCSTATS *stats)
{
  size_t sz = schema->size;
  /* Copy min */
  memcpy(buf, stats->min.data, sz);
  /* Copy max */
  memcpy(buf + sz, stats->max.data, sz);
  /* Copy avg */
  memcpy(buf + 2 * sz, stats->avg.data, sz);

  return sz * 3;
}

/**
 * Stats are always three PCPOINT serializations in a row,
 * min, max, avg. Their size is the uncompressed buffer size for
 * a point, the schema->size.
 */
PCSTATS *pc_patch_stats_deserialize(const PCSCHEMA *schema, const uint8_t *buf)
{
  size_t sz = schema->size;
  const uint8_t *buf_min = buf;
  const uint8_t *buf_max = buf + sz;
  const uint8_t *buf_avg = buf + 2 * sz;

  return pc_stats_new_from_data(schema, buf_min, buf_max, buf_avg);
}

static SERIALIZED_PATCH *pc_patch_dimensional_serialize(const PCPATCH *patch_in)
{
  //  uint32_t size;
  //  uint32_t pcid;
  //  uint32_t compression;
  //  uint32_t npoints;
  //  double xmin, xmax, ymin, ymax;
  //  data:
  //    pcpoint[3] stats;
  //    serialized_pcbytes[ndims] dimensions;

  int i;
  uint8_t *buf;
  size_t serpch_size = pc_patch_serialized_size(patch_in);
  SERIALIZED_PATCH *serpch = pcalloc(serpch_size);
  const PCPATCH_DIMENSIONAL *patch = (PCPATCH_DIMENSIONAL *)patch_in;

  assert(patch_in);
  assert(patch_in->type == PC_DIMENSIONAL);

  /* Copy basics */
  serpch->pcid = patch->schema->pcid;
  serpch->npoints = patch->npoints;
  serpch->bounds = patch->bounds;
  serpch->compression = patch->type;

  /* Get a pointer to the data area */
  buf = serpch->data;

  /* Write stats into the buffer */
  if (patch->stats)
  {
    buf += pc_patch_stats_serialize(buf, patch->schema, patch->stats);
  }
  else
  {
    pcerror("%s: stats missing!", __func__);
  }

  /* Write each dimension in after the stats */
  for (i = 0; i < patch->schema->ndims; i++)
  {
    size_t bsize = 0;
    PCBYTES *pcb = &(patch->bytes[i]);
    pc_bytes_serialize(pcb, buf, &bsize);
    buf += bsize;
  }

  SET_VARSIZE(serpch, serpch_size);
  return serpch;
}

static SERIALIZED_PATCH *pc_patch_lazperf_serialize(const PCPATCH *patch_in)
{
  size_t serpch_size = pc_patch_serialized_size(patch_in);
  SERIALIZED_PATCH *serpch = pcalloc(serpch_size);
  const PCPATCH_LAZPERF *patch = (PCPATCH_LAZPERF *)patch_in;
  uint32_t lazsize = patch->lazperfsize;
  uint8_t *buf = serpch->data;

  assert(patch);
  assert(patch->type == PC_LAZPERF);

  /* Copy basics */
  serpch->pcid = patch->schema->pcid;
  serpch->npoints = patch->npoints;
  serpch->bounds = patch->bounds;
  serpch->compression = patch->type;

  /* Write stats into the buffer first */
  if (patch->stats)
  {
    buf += pc_patch_stats_serialize(buf, patch->schema, patch->stats);
  }
  else
  {
    pcerror("%s: stats missing!", __func__);
  }

  /* Write buffer size */
  memcpy(buf, &(lazsize), 4);
  buf += 4;

  /* Write buffer */
  memcpy(buf, patch->lazperf, patch->lazperfsize);
  SET_VARSIZE(serpch, serpch_size);

  return serpch;
}

static SERIALIZED_PATCH *
pc_patch_uncompressed_serialize(const PCPATCH *patch_in)
{
  //  uint32_t size;
  //  uint32_t pcid;
  //  uint32_t compression;
  //  uint32_t npoints;
  //  double xmin, xmax, ymin, ymax;
  //  data:
  //    pcpoint [];

  uint8_t *buf;
  size_t serpch_size;
  SERIALIZED_PATCH *serpch;
  const PCPATCH_UNCOMPRESSED *patch = (PCPATCH_UNCOMPRESSED *)patch_in;

  serpch_size = pc_patch_serialized_size(patch_in);
  serpch = pcalloc(serpch_size);

  /* Copy basic */
  serpch->compression = patch->type;
  serpch->pcid = patch->schema->pcid;
  serpch->npoints = patch->npoints;
  serpch->bounds = patch->bounds;

  /* Write stats into the buffer first */
  buf = serpch->data;
  if (patch->stats)
  {
    buf += pc_patch_stats_serialize(buf, patch->schema, patch->stats);
  }
  else
  {
    pcerror("%s: stats missing!", __func__);
  }

  /* Copy point list into data buffer */
  memcpy(buf, patch->data, patch->datasize);
  SET_VARSIZE(serpch, serpch_size);
  return serpch;
}

/**
 * Convert struct to byte array.
 * Userdata is currently only PCDIMSTATS, hopefully updated across
 * a number of iterations and saved.
 */
SERIALIZED_PATCH *pc_patch_serialize(const PCPATCH *patch_in, void *userdata)
{
  PCPATCH *patch = (PCPATCH *)patch_in;
  SERIALIZED_PATCH *serpatch = NULL;
  /*
   * Ensure the patch has stats calculated before going on
   */
  if (!patch->stats)
  {
    pcerror("%s: patch is missing stats", __func__);
    return NULL;
  }
  /*
   * Convert the patch to the final target compression,
   * which is the one in the schema.
   */
  if (patch->type != patch->schema->compression)
  {
    patch = pc_patch_compress(patch_in, userdata);
  }

  switch (patch->type)
  {
  case PC_NONE:
  {
    serpatch = pc_patch_uncompressed_serialize(patch);
    break;
  }
  case PC_DIMENSIONAL:
  {
    serpatch = pc_patch_dimensional_serialize(patch);
    break;
  }
  case PC_LAZPERF:
  {
    serpatch = pc_patch_lazperf_serialize(patch);
    break;
  }
  default:
  {
    pcerror("%s: unsupported compression type %d", __func__, patch->type);
  }
  }

  if (patch != patch_in)
    pc_patch_free(patch);

  return serpatch;
}

/**
 * Convert struct to byte array.
 * Userdata is currently only PCDIMSTATS, hopefully updated across
 * a number of iterations and saved.
 */
SERIALIZED_PATCH *pc_patch_serialize_to_uncompressed(const PCPATCH *patch_in)
{
  PCPATCH *patch = (PCPATCH *)patch_in;
  SERIALIZED_PATCH *serpatch;

  /*  Convert the patch to uncompressed, if necessary */
  if (patch->type != PC_NONE)
  {
    patch = pc_patch_uncompress(patch_in);
  }

  serpatch = pc_patch_uncompressed_serialize(patch);

  /* An uncompressed input won't result in a copy */
  if (patch != patch_in)
    pc_patch_free(patch);

  return serpatch;
}

static PCPATCH *
pc_patch_uncompressed_deserialize(const SERIALIZED_PATCH *serpatch,
                                  const PCSCHEMA *schema)
{
  // typedef struct
  // {
  //  uint32_t size;
  //  uint32_t pcid;
  //  uint32_t compression;
  //  uint32_t npoints;
  //  double xmin, xmax, ymin, ymax;
  //  data:
  //    pcpoint[3] pcstats(min, max, avg)
  //    pcpoint[npoints]
  // }
  // SERIALIZED_PATCH;

  uint8_t *buf;
  size_t stats_size = pc_stats_size(schema); // 3 pcpoints worth of stats
  PCPATCH_UNCOMPRESSED *patch = pcalloc(sizeof(PCPATCH_UNCOMPRESSED));

  /* Set up basic info */
  patch->type = serpatch->compression;
  patch->schema = schema;
  patch->readonly = true;
  patch->npoints = serpatch->npoints;
  patch->maxpoints = 0;
  patch->bounds = serpatch->bounds;

  buf = (uint8_t *)serpatch->data;

  /* Point into the stats area */
  patch->stats = pc_patch_stats_deserialize(schema, buf);

  /* Advance data pointer past the stats serialization */
  patch->data = buf + stats_size;

  /* Calculate the point data buffer size */
  patch->datasize = VARSIZE(serpatch) - BUFFERALIGN(sizeof(SERIALIZED_PATCH)) +
                    1 - stats_size;
  if (patch->datasize != patch->npoints * schema->size)
    pcerror("%s: calculated patch data sizes don't match (%d != %d)", __func__,
            patch->datasize, patch->npoints * schema->size);

  return (PCPATCH *)patch;
}

static PCPATCH *
pc_patch_dimensional_deserialize(const SERIALIZED_PATCH *serpatch,
                                 const PCSCHEMA *schema)
{
  // typedef struct
  // {
  //  uint32_t size;
  //  uint32_t pcid;
  //  uint32_t compression;
  //  uint32_t npoints;
  //  double xmin, xmax, ymin, ymax;
  //  data:
  //    pcpoint[3] pcstats(min, max, avg)
  //    pcbytes[ndims];
  // }
  // SERIALIZED_PATCH;

  PCPATCH_DIMENSIONAL *patch;
  int i;
  const uint8_t *buf;
  int ndims = schema->ndims;
  int npoints = serpatch->npoints;
  size_t stats_size = pc_stats_size(schema); // 3 pcpoints worth of stats

  /* Reference the external data */
  patch = pcalloc(sizeof(PCPATCH_DIMENSIONAL));

  /* Set up basic info */
  patch->type = serpatch->compression;
  patch->schema = schema;
  patch->readonly = true;
  patch->npoints = npoints;
  patch->bounds = serpatch->bounds;

  /* Point into the stats area */
  patch->stats = pc_patch_stats_deserialize(schema, serpatch->data);

  /* Set up dimensions */
  patch->bytes = pcalloc(ndims * sizeof(PCBYTES));
  buf = serpatch->data + stats_size;

  for (i = 0; i < ndims; i++)
  {
    PCBYTES *pcb = &(patch->bytes[i]);
    PCDIMENSION *dim = schema->dims[i];
    pc_bytes_deserialize(buf, dim, pcb, true /*readonly*/,
                         false /*flipendian*/);
    pcb->npoints = npoints;
    buf += pc_bytes_serialized_size(pcb);
  }

  return (PCPATCH *)patch;
}

/*
 * We don't do any radical deserialization here. Don't build out the tree, just
 * set up pointers to the start of the buffer, so we can build it out later
 * if necessary.
 */
static PCPATCH *pc_patch_lazperf_deserialize(const SERIALIZED_PATCH *serpatch,
                                             const PCSCHEMA *schema)
{
  PCPATCH_LAZPERF *patch;
  uint32_t lazperfsize;
  int npoints = serpatch->npoints;
  size_t stats_size = pc_stats_size(schema);
  uint8_t *buf = (uint8_t *)serpatch->data + stats_size;

  /* Reference the external data */
  patch = pcalloc(sizeof(PCPATCH_LAZPERF));

  /* Set up basic info */
  patch->type = serpatch->compression;
  patch->schema = schema;
  patch->readonly = true;
  patch->npoints = npoints;
  patch->bounds = serpatch->bounds;

  /* Point into the stats area */
  patch->stats = pc_patch_stats_deserialize(schema, serpatch->data);

  /* Set up buffer */
  memcpy(&lazperfsize, buf, 4);
  patch->lazperfsize = lazperfsize;
  buf += 4;

  patch->lazperf = pcalloc(patch->lazperfsize);
  memcpy(patch->lazperf, buf, patch->lazperfsize);

  return (PCPATCH *)patch;
}

PCPATCH *pc_patch_deserialize(const SERIALIZED_PATCH *serpatch,
                              const PCSCHEMA *schema)
{
  switch (serpatch->compression)
  {
  case PC_NONE:
    return pc_patch_uncompressed_deserialize(serpatch, schema);
  case PC_DIMENSIONAL:
    return pc_patch_dimensional_deserialize(serpatch, schema);
  case PC_LAZPERF:
    return pc_patch_lazperf_deserialize(serpatch, schema);
  }
  pcerror("%s: unsupported compression type", __func__);
  return NULL;
}

static uint8_t *pc_patch_wkb_set_double(uint8_t *wkb, double d)
{
  memcpy(wkb, &d, 8);
  wkb += 8;
  return wkb;
}

static uint8_t *pc_patch_wkb_set_int32(uint8_t *wkb, uint32_t i)
{
  memcpy(wkb, &i, 4);
  wkb += 4;
  return wkb;
}

static uint8_t *pc_patch_wkb_set_char(uint8_t *wkb, char c)
{
  memcpy(wkb, &c, 1);
  wkb += 1;
  return wkb;
}

/* 0 = xdr | big endian */
/* 1 = ndr | little endian */
static char machine_endian(void)
{
  static int check_int = 1; /* dont modify this!!! */
  return *((char *)&check_int);
}

uint8_t *pc_patch_to_geometry_wkb_envelope(const SERIALIZED_PATCH *pa,
                                           const PCSCHEMA *schema,
                                           size_t *wkbsize)
{
  static uint32_t srid_mask = 0x20000000;
  static uint32_t nrings = 1;
  static uint32_t npoints = 5;
  uint32_t wkbtype = 3; /* WKB POLYGON */
  uint8_t *wkb, *ptr;
  int has_srid = false;
  size_t size =
      1 + 4 + 4 + 4 +
      2 * npoints * 8; /* endian + type + nrings + npoints + 5 dbl pts */

  /* Bounds! */
  double xmin = pa->bounds.xmin;
  double ymin = pa->bounds.ymin;
  double xmax = pa->bounds.xmax;
  double ymax = pa->bounds.ymax;

  /* Make sure they're slightly bigger than a point */
  if (xmin == xmax)
    xmax += xmax * 0.0000001;
  if (ymin == ymax)
    ymax += ymax * 0.0000001;

  if (schema->srid > 0)
  {
    has_srid = true;
    wkbtype |= srid_mask;
    size += 4;
  }

  wkb = palloc(size);
  ptr = wkb;

  ptr = pc_patch_wkb_set_char(ptr, machine_endian()); /* Endian flag */

  ptr = pc_patch_wkb_set_int32(ptr, wkbtype); /* TYPE = Polygon */

  if (has_srid)
  {
    ptr = pc_patch_wkb_set_int32(ptr, schema->srid); /* SRID */
  }

  ptr = pc_patch_wkb_set_int32(ptr, nrings);  /* NRINGS = 1 */
  ptr = pc_patch_wkb_set_int32(ptr, npoints); /* NPOINTS = 5 */

  /* Point 0 */
  ptr = pc_patch_wkb_set_double(ptr, pa->bounds.xmin);
  ptr = pc_patch_wkb_set_double(ptr, pa->bounds.ymin);

  /* Point 1 */
  ptr = pc_patch_wkb_set_double(ptr, pa->bounds.xmin);
  ptr = pc_patch_wkb_set_double(ptr, pa->bounds.ymax);

  /* Point 2 */
  ptr = pc_patch_wkb_set_double(ptr, pa->bounds.xmax);
  ptr = pc_patch_wkb_set_double(ptr, pa->bounds.ymax);

  /* Point 3 */
  ptr = pc_patch_wkb_set_double(ptr, pa->bounds.xmax);
  ptr = pc_patch_wkb_set_double(ptr, pa->bounds.ymin);

  /* Point 4 */
  ptr = pc_patch_wkb_set_double(ptr, pa->bounds.xmin);
  ptr = pc_patch_wkb_set_double(ptr, pa->bounds.ymin);

  if (wkbsize)
    *wkbsize = size;
  return wkb;
}
