#include "gdal_common.hpp"

#include "gdal_coordinate_transformation.hpp"
#include "gdal_geometry.hpp"
#include "gdal_geometrycollection.hpp"
#include "gdal_linearring.hpp"
#include "gdal_linestring.hpp"
#include "gdal_multilinestring.hpp"
#include "gdal_multipoint.hpp"
#include "gdal_multipolygon.hpp"
#include "gdal_point.hpp"
#include "gdal_polygon.hpp"
#include "gdal_spatial_reference.hpp"

#include <node_buffer.h>
#include <ogr_core.h>
#include <sstream>
#include <stdlib.h>

namespace node_gdal {

Nan::Persistent<FunctionTemplate> Geometry::constructor;

void Geometry::Initialize(Local<Object> target) {
  Nan::HandleScope scope;

  Local<FunctionTemplate> lcons = Nan::New<FunctionTemplate>(Geometry::New);
  lcons->InstanceTemplate()->SetInternalFieldCount(1);
  lcons->SetClassName(Nan::New("Geometry").ToLocalChecked());

  // Nan::SetMethod(constructor, "fromWKBType", Geometry::create);
  Nan::SetMethod(lcons, "fromWKT", Geometry::createFromWkt);
  Nan::SetMethod(lcons, "fromWKB", Geometry::createFromWkb);
  Nan::SetMethod(lcons, "fromGeoJson", Geometry::createFromGeoJson);
  Nan::SetMethod(lcons, "getName", Geometry::getName);
  Nan::SetMethod(lcons, "getConstructor", Geometry::getConstructor);

  Nan::SetPrototypeMethod(lcons, "toString", toString);
  Nan::SetPrototypeMethod(lcons, "toKML", exportToKML);
  Nan::SetPrototypeMethod(lcons, "toGML", exportToGML);
  Nan::SetPrototypeMethod(lcons, "toJSON", exportToJSON);
  Nan::SetPrototypeMethod(lcons, "toWKT", exportToWKT);
  Nan::SetPrototypeMethod(lcons, "toWKB", exportToWKB);
  Nan::SetPrototypeMethod(lcons, "isEmpty", isEmpty);
  Nan::SetPrototypeMethod(lcons, "isValid", isValid);
  Nan::SetPrototypeMethod(lcons, "isSimple", isSimple);
  Nan::SetPrototypeMethod(lcons, "isRing", isRing);
  Nan::SetPrototypeMethod(lcons, "clone", clone);
  Nan::SetPrototypeMethod(lcons, "empty", empty);
  Nan::SetPrototypeMethod(lcons, "closeRings", closeRings);
  Nan::SetPrototypeMethod(lcons, "intersects", intersects);
  Nan::SetPrototypeMethod(lcons, "equals", equals);
  Nan::SetPrototypeMethod(lcons, "disjoint", disjoint);
  Nan::SetPrototypeMethod(lcons, "touches", touches);
  Nan::SetPrototypeMethod(lcons, "crosses", crosses);
  Nan::SetPrototypeMethod(lcons, "within", within);
  Nan::SetPrototypeMethod(lcons, "contains", contains);
  Nan::SetPrototypeMethod(lcons, "overlaps", overlaps);
  Nan::SetPrototypeMethod(lcons, "boundary", boundary);
  Nan::SetPrototypeMethod(lcons, "distance", distance);
  Nan::SetPrototypeMethod(lcons, "convexHull", convexHull);
  Nan::SetPrototypeMethod(lcons, "buffer", buffer);
  Nan::SetPrototypeMethod(lcons, "intersection", intersection);
  Nan::SetPrototypeMethod(lcons, "union", unionGeometry);
  Nan::SetPrototypeMethod(lcons, "difference", difference);
  Nan::SetPrototypeMethod(lcons, "symDifference", symDifference);
  Nan::SetPrototypeMethod(lcons, "centroid", centroid);
  Nan::SetPrototypeMethod(lcons, "simplify", simplify);
  Nan::SetPrototypeMethod(lcons, "simplifyPreserveTopology", simplifyPreserveTopology);
  Nan::SetPrototypeMethod(lcons, "segmentize", segmentize);
  Nan::SetPrototypeMethod(lcons, "swapXY", swapXY);
  Nan::SetPrototypeMethod(lcons, "getEnvelope", getEnvelope);
  Nan::SetPrototypeMethod(lcons, "getEnvelope3D", getEnvelope3D);
  Nan::SetPrototypeMethod(lcons, "transform", transform);
  Nan::SetPrototypeMethod(lcons, "transformTo", transformTo);

  ATTR(lcons, "srs", srsGetter, srsSetter);
  ATTR(lcons, "wkbSize", wkbSizeGetter, READ_ONLY_SETTER);
  ATTR(lcons, "dimension", dimensionGetter, READ_ONLY_SETTER);
  ATTR(lcons, "coordinateDimension", coordinateDimensionGetter, coordinateDimensionSetter);
  ATTR(lcons, "wkbType", typeGetter, READ_ONLY_SETTER);
  ATTR(lcons, "name", nameGetter, READ_ONLY_SETTER);

  Nan::Set(target, Nan::New("Geometry").ToLocalChecked(), Nan::GetFunction(lcons).ToLocalChecked());

  constructor.Reset(lcons);
}

Geometry::Geometry(OGRGeometry *geom) : Nan::ObjectWrap(), this_(geom), owned_(true), size_(0) {
  LOG("Created Geometry [%p]", geom);
}

Geometry::Geometry() : Nan::ObjectWrap(), this_(NULL), owned_(true), size_(0) {
}

Geometry::~Geometry() {
  if (this_) {
    LOG("Disposing Geometry [%p] (%s)", this_, owned_ ? "owned" : "unowned");
    if (owned_) {
      OGRGeometryFactory::destroyGeometry(this_);
      Nan::AdjustExternalMemory(-size_);
    }
    LOG("Disposed Geometry [%p]", this_)
    this_ = NULL;
  }
}

/**
 * Abstract base class for all geometry classes.
 *
 * @class gdal.Geometry
 */
NAN_METHOD(Geometry::New) {
  Nan::HandleScope scope;
  Geometry *f;

  if (!info.IsConstructCall()) {
    Nan::ThrowError("Cannot call constructor as function, you need to use 'new' keyword");
    return;
  }

  if (info[0]->IsExternal()) {
    Local<External> ext = info[0].As<External>();
    void *ptr = ext->Value();
    f = static_cast<Geometry *>(ptr);

  } else {
    Nan::ThrowError(
      "Geometry doesnt have a constructor, use Geometry.fromWKT(), Geometry.fromWKB() or type-specific constructor. ie. new ogr.Point()");
    return;
    // OGRwkbGeometryType geometry_type;
    // NODE_ARG_ENUM(0, "geometry type", OGRwkbGeometryType, geometry_type);
    // OGRGeometry *geom = OGRGeometryFactory::createGeometry(geometry_type);
    // f = new Geometry(geom);
  }

  f->Wrap(info.This());
  info.GetReturnValue().Set(info.This());
}

Local<Value> Geometry::New(OGRGeometry *geom) {
  Nan::EscapableHandleScope scope;
  return scope.Escape(Geometry::New(geom, true));
}

Local<Value> Geometry::New(OGRGeometry *geom, bool owned) {
  Nan::EscapableHandleScope scope;

  if (!geom) { return scope.Escape(Nan::Null()); }

  OGRwkbGeometryType type = getGeometryType_fixed(geom);
  type = wkbFlatten(type);

  switch (type) {
    case wkbPoint: return scope.Escape(Point::New(static_cast<OGRPoint *>(geom), owned));
    case wkbLineString: return scope.Escape(LineString::New(static_cast<OGRLineString *>(geom), owned));
    case wkbLinearRing: return scope.Escape(LinearRing::New(static_cast<OGRLinearRing *>(geom), owned));
    case wkbPolygon: return scope.Escape(Polygon::New(static_cast<OGRPolygon *>(geom), owned));
    case wkbGeometryCollection:
      return scope.Escape(GeometryCollection::New(static_cast<OGRGeometryCollection *>(geom), owned));
    case wkbMultiPoint: return scope.Escape(MultiPoint::New(static_cast<OGRMultiPoint *>(geom), owned));
    case wkbMultiLineString: return scope.Escape(MultiLineString::New(static_cast<OGRMultiLineString *>(geom), owned));
    case wkbMultiPolygon: return scope.Escape(MultiPolygon::New(static_cast<OGRMultiPolygon *>(geom), owned));
    default: Nan::ThrowError("Tried to create unsupported geometry type"); return scope.Escape(Nan::Undefined());
  }
}

OGRwkbGeometryType Geometry::getGeometryType_fixed(OGRGeometry *geom) {
  // For some reason OGRLinearRing::getGeometryType uses OGRLineString's
  // method... meaning OGRLinearRing::getGeometryType returns wkbLineString

  // http://trac.osgeo.org/gdal/ticket/1755

  OGRwkbGeometryType type = geom->getGeometryType();

  if (std::string(geom->getGeometryName()) == "LINEARRING") {
    type = (OGRwkbGeometryType)(wkbLinearRing | (type & wkb25DBit));
  }

  return type;
}

NAN_METHOD(Geometry::toString) {
  Nan::HandleScope scope;
  Geometry *geom = Nan::ObjectWrap::Unwrap<Geometry>(info.This());
  std::ostringstream ss;
  ss << "Geometry (" << geom->this_->getGeometryName() << ")";
  info.GetReturnValue().Set(Nan::New(ss.str().c_str()).ToLocalChecked());
}

/**
 * Closes any un-closed rings.
 *
 * @method closeRings
 */
NODE_WRAPPED_METHOD(Geometry, closeRings, closeRings);

/**
 * Clears the geometry.
 *
 * @method empty
 */
NODE_WRAPPED_METHOD(Geometry, empty, empty);

/**
 * Swaps x, y coordinates.
 *
 * @method swapXY
 */
NODE_WRAPPED_METHOD(Geometry, swapXY, swapXY);

/**
 * Determines if the geometry is empty.
 *
 * @method isEmpty
 * @return Boolean
 */
NODE_WRAPPED_METHOD_WITH_RESULT(Geometry, isEmpty, Boolean, IsEmpty);

/**
 * Determines if the geometry is valid.
 *
 * @method isValid
 * @return Boolean
 */
NODE_WRAPPED_METHOD_WITH_RESULT(Geometry, isValid, Boolean, IsValid);

/**
 * Determines if the geometry is simple.
 *
 * @method isSimple
 * @return Boolean
 */
NODE_WRAPPED_METHOD_WITH_RESULT(Geometry, isSimple, Boolean, IsSimple);

/**
 * Determines if the geometry is a ring.
 *
 * @method isRing
 * @return Boolean
 */
NODE_WRAPPED_METHOD_WITH_RESULT(Geometry, isRing, Boolean, IsRing);

/**
 * Determines if the two geometries intersect.
 *
 * @method intersects
 * @param {gdal.Geometry} geometry
 * @return Boolean
 */
NODE_WRAPPED_METHOD_WITH_RESULT_1_WRAPPED_PARAM(
  Geometry, intersects, Boolean, Intersects, Geometry, "geometry to compare");

/**
 * Determines if the two geometries equal each other.
 *
 * @method equals
 * @param {gdal.Geometry} geometry
 * @return Boolean
 */
NODE_WRAPPED_METHOD_WITH_RESULT_1_WRAPPED_PARAM(Geometry, equals, Boolean, Equals, Geometry, "geometry to compare");

/**
 * Determines if the two geometries are disjoint.
 *
 * @method disjoint
 * @param {gdal.Geometry} geometry
 * @return Boolean
 */
NODE_WRAPPED_METHOD_WITH_RESULT_1_WRAPPED_PARAM(Geometry, disjoint, Boolean, Disjoint, Geometry, "geometry to compare");

/**
 * Determines if the two geometries touch.
 *
 * @method touches
 * @param {gdal.Geometry} geometry
 * @return Boolean
 */
NODE_WRAPPED_METHOD_WITH_RESULT_1_WRAPPED_PARAM(Geometry, touches, Boolean, Touches, Geometry, "geometry to compare");

/**
 * Determines if the two geometries cross.
 *
 * @method crosses
 * @param {gdal.Geometry} geometry
 * @return Boolean
 */
NODE_WRAPPED_METHOD_WITH_RESULT_1_WRAPPED_PARAM(Geometry, crosses, Boolean, Crosses, Geometry, "geometry to compare");

/**
 * Determines if the current geometry is within the provided geometry.
 *
 * @method within
 * @param {gdal.Geometry} geometry
 * @return Boolean
 */
NODE_WRAPPED_METHOD_WITH_RESULT_1_WRAPPED_PARAM(Geometry, within, Boolean, Within, Geometry, "geometry to compare");

/**
 * Determines if the current geometry contains the provided geometry.
 *
 * @method contains
 * @param {gdal.Geometry} geometry
 * @return Boolean
 */
NODE_WRAPPED_METHOD_WITH_RESULT_1_WRAPPED_PARAM(Geometry, contains, Boolean, Contains, Geometry, "geometry to compare");

/**
 * Determines if the current geometry overlaps the provided geometry.
 *
 * @method overlaps
 * @param {gdal.Geometry} geometry
 * @return Boolean
 */
NODE_WRAPPED_METHOD_WITH_RESULT_1_WRAPPED_PARAM(Geometry, overlaps, Boolean, Overlaps, Geometry, "geometry to compare");

/**
 * Computes the distance between the two geometries.
 *
 * @method distance
 * @param {gdal.Geometry} geometry
 * @return Number
 */
NODE_WRAPPED_METHOD_WITH_RESULT_1_WRAPPED_PARAM(
  Geometry, distance, Number, Distance, Geometry, "geometry to use for distance calculation");

/**
 * Modify the geometry such it has no segment longer then the given distance.
 *
 * @method segmentize
 * @param {Number} segment_length
 * @return Number
 */
NODE_WRAPPED_METHOD_WITH_1_DOUBLE_PARAM(Geometry, segmentize, segmentize, "segment length");

/**
 * Apply arbitrary coordinate transformation to the geometry.
 *
 * This method will transform the coordinates of a geometry from their current
 * spatial reference system to a new target spatial reference system. Normally
 * this means reprojecting the vectors, but it could include datum shifts,
 * and changes of units.
 *
 * Note that this method does not require that the geometry already have a
 * spatial reference system. It will be assumed that they can be treated as
 * having the source spatial reference system of the {{#crossLink
 * "gdal.CoordinateTransformation"}}CoordinateTransformation{{/crossLink}}
 * object, and the actual SRS of the geometry will be ignored.
 *
 * @throws Error
 * @method transform
 * @param {gdal.CoordinateTransformation} transformation
 */
NODE_WRAPPED_METHOD_WITH_OGRERR_RESULT_1_WRAPPED_PARAM(
  Geometry, transform, transform, CoordinateTransformation, "transform");

/**
 * Transforms the geometry to match the provided {{#crossLink
 * "gdal.SpatialReference"}}SpatialReference{{/crossLink}}.
 *
 * @throws Error
 * @method transformTo
 * @param {gdal.SpatialReference} srs
 */
NODE_WRAPPED_METHOD_WITH_OGRERR_RESULT_1_WRAPPED_PARAM(
  Geometry, transformTo, transformTo, SpatialReference, "spatial reference");

/**
 * Clones the instance.
 *
 * @method clone
 * @return gdal.Geometry
 */
NAN_METHOD(Geometry::clone) {
  Nan::HandleScope scope;
  Geometry *geom = Nan::ObjectWrap::Unwrap<Geometry>(info.This());
  info.GetReturnValue().Set(Geometry::New(geom->this_->clone()));
}

/**
 * Compute convex hull.
 *
 * @method convexHull
 * @return gdal.Geometry
 */
NAN_METHOD(Geometry::convexHull) {
  Nan::HandleScope scope;
  Geometry *geom = Nan::ObjectWrap::Unwrap<Geometry>(info.This());
  info.GetReturnValue().Set(Geometry::New(geom->this_->ConvexHull()));
}

/**
 * Compute boundary.
 *
 * @method boundary
 * @return gdal.Geometry
 */
NAN_METHOD(Geometry::boundary) {
  Nan::HandleScope scope;
  Geometry *geom = Nan::ObjectWrap::Unwrap<Geometry>(info.This());
  info.GetReturnValue().Set(Geometry::New(geom->this_->Boundary()));
}

/**
 * Compute intersection with another geometry.
 *
 * @method intersection
 * @param {gdal.Geometry} geometry
 * @return gdal.Geometry
 */
NAN_METHOD(Geometry::intersection) {
  Nan::HandleScope scope;

  Geometry *geom = Nan::ObjectWrap::Unwrap<Geometry>(info.This());
  Geometry *x = NULL;

  NODE_ARG_WRAPPED(0, "geometry to use for intersection", Geometry, x);

  info.GetReturnValue().Set(Geometry::New(geom->this_->Intersection(x->this_)));
}

/**
 * Compute the union of this geometry with another.
 *
 * @method union
 * @param {gdal.Geometry} geometry
 * @return gdal.Geometry
 */
NAN_METHOD(Geometry::unionGeometry) {
  Nan::HandleScope scope;

  Geometry *geom = Nan::ObjectWrap::Unwrap<Geometry>(info.This());
  Geometry *x = NULL;

  NODE_ARG_WRAPPED(0, "geometry to use for union", Geometry, x);

  info.GetReturnValue().Set(Geometry::New(geom->this_->Union(x->this_)));
}

/**
 * Compute the difference of this geometry with another.
 *
 * @method difference
 * @param {gdal.Geometry} geometry
 * @return gdal.Geometry
 */
NAN_METHOD(Geometry::difference) {
  Nan::HandleScope scope;

  Geometry *geom = Nan::ObjectWrap::Unwrap<Geometry>(info.This());
  Geometry *x = NULL;

  NODE_ARG_WRAPPED(0, "geometry to use for difference", Geometry, x);

  info.GetReturnValue().Set(Geometry::New(geom->this_->Difference(x->this_)));
}

/**
 * Computes the symmetric difference of this geometry and the second geometry.
 *
 * @method symDifference
 * @param {gdal.Geometry} geometry
 * @return gdal.Geometry
 */
NAN_METHOD(Geometry::symDifference) {
  Nan::HandleScope scope;

  Geometry *geom = Nan::ObjectWrap::Unwrap<Geometry>(info.This());
  Geometry *x = NULL;

  NODE_ARG_WRAPPED(0, "geometry to use for symDifference", Geometry, x);

  info.GetReturnValue().Set(Geometry::New(geom->this_->SymDifference(x->this_)));
}

/**
 * Reduces the geometry complexity.
 *
 * @method simplify
 * @param {Number} tolerance
 * @return gdal.Geometry
 */
NAN_METHOD(Geometry::simplify) {
  Nan::HandleScope scope;

  double tolerance;

  NODE_ARG_DOUBLE(0, "tolerance", tolerance);

  Geometry *geom = Nan::ObjectWrap::Unwrap<Geometry>(info.This());

  info.GetReturnValue().Set(Geometry::New(geom->this_->Simplify(tolerance)));
}

/**
 * Reduces the geometry complexity while preserving the topology.
 *
 * @method simplifyPreserveTopology
 * @param {Number} tolerance
 * @return gdal.Geometry
 */
NAN_METHOD(Geometry::simplifyPreserveTopology) {
  Nan::HandleScope scope;

  double tolerance;

  NODE_ARG_DOUBLE(0, "tolerance", tolerance);

  Geometry *geom = Nan::ObjectWrap::Unwrap<Geometry>(info.This());

  info.GetReturnValue().Set(Geometry::New(geom->this_->SimplifyPreserveTopology(tolerance)));
}

/**
 * Buffers the geometry by the given distance.
 *
 * @method buffer
 * @param {Number} distance
 * @param {integer} segments
 * @return gdal.Geometry
 */
NAN_METHOD(Geometry::buffer) {
  Nan::HandleScope scope;

  double distance;
  int number_of_segments = 30;

  NODE_ARG_DOUBLE(0, "distance", distance);
  NODE_ARG_INT_OPT(1, "number of segments", number_of_segments);

  Geometry *geom = Nan::ObjectWrap::Unwrap<Geometry>(info.This());

  info.GetReturnValue().Set(Geometry::New(geom->this_->Buffer(distance, number_of_segments)));
}

/**
 * Convert a geometry into well known text format.
 *
 * @method toWKT
 * @return gdal.Geometry
 */
NAN_METHOD(Geometry::exportToWKT) {
  Nan::HandleScope scope;

  Geometry *geom = Nan::ObjectWrap::Unwrap<Geometry>(info.This());

  char *text = NULL;
  OGRErr err = geom->this_->exportToWkt(&text);

  if (err) {
    NODE_THROW_OGRERR(err);
    return;
  }
  if (text) {
    info.GetReturnValue().Set(SafeString::New(text));
    return;
  }

  return;
}

/**
 * Convert a geometry into well known binary format.
 *
 * @method toWKB
 * @param {string} [byte_order="MSB"] ({{#crossLink "Constants
 * (wkbByteOrder)"}}see options{{/crossLink}})
 * @param {string} [variant="OGC"] ({{#crossLink "Constants (wkbVariant)"}}see
 * options{{/crossLink}})
 * @return gdal.Geometry
 */
NAN_METHOD(Geometry::exportToWKB) {
  Nan::HandleScope scope;

  Geometry *geom = Nan::ObjectWrap::Unwrap<Geometry>(info.This());

  int size = geom->this_->WkbSize();
  unsigned char *data = (unsigned char *)malloc(size);

  // byte order
  OGRwkbByteOrder byte_order;
  std::string order = "MSB";
  NODE_ARG_OPT_STR(0, "byte order", order);
  if (order == "MSB") {
    byte_order = wkbXDR;
  } else if (order == "LSB") {
    byte_order = wkbNDR;
  } else {
    Nan::ThrowError("byte order must be 'MSB' or 'LSB'");
    return;
  }

#if GDAL_VERSION_MAJOR > 1 || (GDAL_VERSION_MINOR > 10)
  // wkb variant
  OGRwkbVariant wkb_variant;
  std::string variant = "OGC";
  NODE_ARG_OPT_STR(1, "wkb variant", variant);
  if (variant == "OGC") {
#if GDAL_VERSION_MAJOR > 1
    wkb_variant = wkbVariantOldOgc;
#else
    wkb_variant = wkbVariantOgc;
#endif
  } else if (variant == "ISO") {
    wkb_variant = wkbVariantIso;
  } else {
    Nan::ThrowError("variant must be 'OGC' or 'ISO'");
    return;
  }
  OGRErr err = geom->this_->exportToWkb(byte_order, data, wkb_variant);
#else
  OGRErr err = geom->this_->exportToWkb(byte_order, data);
#endif

  //^^ export to wkb and fill buffer ^^
  if (err) {
    free(data);
    NODE_THROW_OGRERR(err);
    return;
  }

  Local<Value> result = Nan::NewBuffer((char *)data, size).ToLocalChecked();
  info.GetReturnValue().Set(result);
}

/**
 * Convert a geometry into KML format.
 *
 * @method toKML
 * @return gdal.Geometry
 */
NAN_METHOD(Geometry::exportToKML) {
  Nan::HandleScope scope;

  Geometry *geom = Nan::ObjectWrap::Unwrap<Geometry>(info.This());

  char *text = geom->this_->exportToKML();
  if (text) {
    Local<Value> result = Nan::New(text).ToLocalChecked();
    CPLFree(text);
    info.GetReturnValue().Set(result);
    return;
  }

  return;
}

/**
 * Convert a geometry into GML format.
 *
 * @method toGML
 * @return gdal.Geometry
 */
NAN_METHOD(Geometry::exportToGML) {
  Nan::HandleScope scope;

  Geometry *geom = Nan::ObjectWrap::Unwrap<Geometry>(info.This());

  char *text = geom->this_->exportToGML();
  if (text) {
    Local<Value> result = Nan::New(text).ToLocalChecked();
    CPLFree(text);
    info.GetReturnValue().Set(result);
    return;
  }

  return;
}

/**
 * Convert a geometry into JSON format.
 *
 * @method toJSON
 * @return gdal.Geometry
 */
NAN_METHOD(Geometry::exportToJSON) {
  Nan::HandleScope scope;

  Geometry *geom = Nan::ObjectWrap::Unwrap<Geometry>(info.This());

  char *text = geom->this_->exportToJson();
  if (text) {
    Local<Value> result = Nan::New(text).ToLocalChecked();
    CPLFree(text);
    info.GetReturnValue().Set(result);
    return;
  }

  return;
}

/**
 * Compute the centroid of the geometry.
 *
 * @method centroid
 * @return gdal.Point
 */
NAN_METHOD(Geometry::centroid) {
  // The Centroid method wants the caller to create the point to fill in.
  // Instead of requiring the caller to create the point geometry to fill in, we
  // new up an OGRPoint and put the result into it and return that.
  Nan::HandleScope scope;
  OGRPoint *point = new OGRPoint();

  Nan::ObjectWrap::Unwrap<Geometry>(info.This())->this_->Centroid(point);

  info.GetReturnValue().Set(Point::New(point));
}

/**
 * Computes the bounding box (envelope).
 *
 * @method getEnvelope
 * @return {gdal.Envelope} Bounding envelope
 */
NAN_METHOD(Geometry::getEnvelope) {
  // returns object containing boundaries until complete OGREnvelope binding is
  // built

  Nan::HandleScope scope;

  Geometry *geom = Nan::ObjectWrap::Unwrap<Geometry>(info.This());

  OGREnvelope *envelope = new OGREnvelope();
  geom->this_->getEnvelope(envelope);

  Local<Object> obj = Nan::New<Object>();
  Nan::Set(obj, Nan::New("minX").ToLocalChecked(), Nan::New<Number>(envelope->MinX));
  Nan::Set(obj, Nan::New("maxX").ToLocalChecked(), Nan::New<Number>(envelope->MaxX));
  Nan::Set(obj, Nan::New("minY").ToLocalChecked(), Nan::New<Number>(envelope->MinY));
  Nan::Set(obj, Nan::New("maxY").ToLocalChecked(), Nan::New<Number>(envelope->MaxY));

  delete envelope;

  info.GetReturnValue().Set(obj);
}

/**
 * Computes the 3D bounding box (envelope).
 *
 * @method getEnvelope3D
 * @return {gdal.Envelope3D} Bounding envelope
 */
NAN_METHOD(Geometry::getEnvelope3D) {
  // returns object containing boundaries until complete OGREnvelope binding is
  // built

  Nan::HandleScope scope;

  Geometry *geom = Nan::ObjectWrap::Unwrap<Geometry>(info.This());

  OGREnvelope3D *envelope = new OGREnvelope3D();
  geom->this_->getEnvelope(envelope);

  Local<Object> obj = Nan::New<Object>();
  Nan::Set(obj, Nan::New("minX").ToLocalChecked(), Nan::New<Number>(envelope->MinX));
  Nan::Set(obj, Nan::New("maxX").ToLocalChecked(), Nan::New<Number>(envelope->MaxX));
  Nan::Set(obj, Nan::New("minY").ToLocalChecked(), Nan::New<Number>(envelope->MinY));
  Nan::Set(obj, Nan::New("maxY").ToLocalChecked(), Nan::New<Number>(envelope->MaxY));
  Nan::Set(obj, Nan::New("minZ").ToLocalChecked(), Nan::New<Number>(envelope->MinZ));
  Nan::Set(obj, Nan::New("maxZ").ToLocalChecked(), Nan::New<Number>(envelope->MaxZ));

  delete envelope;

  info.GetReturnValue().Set(obj);
}

// --- JS static methods (OGRGeometryFactory) ---

/**
 * Creates a Geometry from a WKT string.
 *
 * @static
 * @method fromWKT
 * @param {String} wkt
 * @param {gdal.SpatialReference} [srs]
 * @return gdal.Geometry
 */
NAN_METHOD(Geometry::createFromWkt) {
  Nan::HandleScope scope;

  std::string wkt_string;
  SpatialReference *srs = NULL;

  NODE_ARG_STR(0, "wkt", wkt_string);
  NODE_ARG_WRAPPED_OPT(1, "srs", SpatialReference, srs);

  OGRChar *wkt = (OGRChar *)wkt_string.c_str();
  OGRGeometry *geom = NULL;
  OGRSpatialReference *ogr_srs = NULL;
  if (srs) { ogr_srs = srs->get(); }

  OGRErr err = OGRGeometryFactory::createFromWkt(&wkt, ogr_srs, &geom);
  if (err) {
    NODE_THROW_OGRERR(err);
    return;
  }

  info.GetReturnValue().Set(Geometry::New(geom, true));
}

/**
 * Creates a Geometry from a WKB buffer.
 *
 * @static
 * @method fromWKB
 * @param {Buffer} wkb
 * @param {gdal.SpatialReference} [srs]
 * @return gdal.Geometry
 */
NAN_METHOD(Geometry::createFromWkb) {
  Nan::HandleScope scope;

  std::string wkb_string;
  SpatialReference *srs = NULL;

  Local<Object> wkb_obj;
  NODE_ARG_OBJECT(0, "wkb", wkb_obj);
  NODE_ARG_WRAPPED_OPT(1, "srs", SpatialReference, srs);

  std::string obj_type = *Nan::Utf8String(wkb_obj->GetConstructorName());

  if (obj_type != "Buffer" && obj_type != "Uint8Array") {
    Nan::ThrowError("Argument must be a buffer object");
    return;
  }

  unsigned char *data = (unsigned char *)Buffer::Data(wkb_obj);
  size_t length = Buffer::Length(wkb_obj);

  OGRGeometry *geom = NULL;
  OGRSpatialReference *ogr_srs = NULL;
  if (srs) { ogr_srs = srs->get(); }

  OGRErr err = OGRGeometryFactory::createFromWkb(data, ogr_srs, &geom, length);
  if (err) {
    NODE_THROW_OGRERR(err);
    return;
  }

  info.GetReturnValue().Set(Geometry::New(geom, true));
}

/**
 * Creates a Geometry from a GeoJSON string.
 *
 * @static
 * @method fromGeoJson
 * @param {Object} geojson
 * @return gdal.Geometry
 */
NAN_METHOD(Geometry::createFromGeoJson) {
  Nan::HandleScope scope;
#if GDAL_VERSION_MAJOR < 2 || (GDAL_VERSION_MAJOR <= 2 && GDAL_VERSION_MINOR < 3)
  Nan::ThrowError("GDAL < 2.3 does not support parsing GeoJSON directly");
  return;
#else

  Local<Object> geo_obj;
  NODE_ARG_OBJECT(0, "geojson", geo_obj);

  // goes to text to pass it in, there isn't a performant way to
  // go from v8 JSON -> CPLJSON anyways
  Nan::JSON NanJSON;
  Nan::MaybeLocal<String> result = NanJSON.Stringify(geo_obj);
  if (result.IsEmpty()) {
    Nan::ThrowError("Invalid GeoJSON");
    return;
  }
  Local<String> stringified = result.ToLocalChecked();
  std::string val = *Nan::Utf8String(stringified);

  OGRGeometry *geom = OGRGeometryFactory::createFromGeoJson(val.c_str());
  info.GetReturnValue().Set(Geometry::New(geom, true));
#endif
}

/**
 * Creates an empty Geometry from a WKB type.
 *
 * @static
 * @method create
 * @param {Integer} type WKB geometry type ({{#crossLink "Constants
 * (wkbGeometryType)"}}available options{{/crossLink}})
 * @return gdal.Geometry
 */
NAN_METHOD(Geometry::create) {
  Nan::HandleScope scope;

  OGRwkbGeometryType type = wkbUnknown;
  NODE_ARG_ENUM(0, "type", OGRwkbGeometryType, type);

  info.GetReturnValue().Set(Geometry::New(OGRGeometryFactory::createGeometry(type), true));
}

/**
 * @attribute srs
 * @type gdal.SpatialReference
 */
NAN_GETTER(Geometry::srsGetter) {
  Nan::HandleScope scope;
  Geometry *geom = Nan::ObjectWrap::Unwrap<Geometry>(info.This());
  info.GetReturnValue().Set(SpatialReference::New(geom->this_->getSpatialReference(), false));
}

NAN_SETTER(Geometry::srsSetter) {
  Nan::HandleScope scope;
  Geometry *geom = Nan::ObjectWrap::Unwrap<Geometry>(info.This());

  OGRSpatialReference *srs = NULL;
  if (IS_WRAPPED(value, SpatialReference)) {
    SpatialReference *srs_obj = Nan::ObjectWrap::Unwrap<SpatialReference>(value.As<Object>());
    srs = srs_obj->get();
  } else if (!value->IsNull() && !value->IsUndefined()) {
    Nan::ThrowError("srs must be SpatialReference object");
    return;
  }

  geom->this_->assignSpatialReference(srs);
}

/**
 * @readOnly
 * @attribute name
 * @type String
 */
NAN_GETTER(Geometry::nameGetter) {
  Nan::HandleScope scope;
  Geometry *geom = Nan::ObjectWrap::Unwrap<Geometry>(info.This());
  info.GetReturnValue().Set(SafeString::New(geom->this_->getGeometryName()));
}

/**
 * See {{#crossLink "Constants
 * (wkbGeometryType)"}}wkbGeometryTypes{{/crossLink}}.
 * @readOnly
 * @attribute wkbType
 * @type integer
 */
NAN_GETTER(Geometry::typeGetter) {
  Nan::HandleScope scope;
  Geometry *geom = Nan::ObjectWrap::Unwrap<Geometry>(info.This());
  info.GetReturnValue().Set(Nan::New<Integer>(getGeometryType_fixed(geom->this_)));
}

/**
 * @readOnly
 * @attribute wkbSize
 * @type Integer
 */
NAN_GETTER(Geometry::wkbSizeGetter) {
  Nan::HandleScope scope;
  Geometry *geom = Nan::ObjectWrap::Unwrap<Geometry>(info.This());
  info.GetReturnValue().Set(Nan::New<Integer>(geom->this_->WkbSize()));
}

/**
 * @readOnly
 * @attribute dimension
 * @type Integer
 */
NAN_GETTER(Geometry::dimensionGetter) {
  Nan::HandleScope scope;
  Geometry *geom = Nan::ObjectWrap::Unwrap<Geometry>(info.This());
  info.GetReturnValue().Set(Nan::New<Integer>(geom->this_->getDimension()));
}

/**
 * @attribute coordinateDimension
 * @type Integer
 */
NAN_GETTER(Geometry::coordinateDimensionGetter) {
  Nan::HandleScope scope;
  Geometry *geom = Nan::ObjectWrap::Unwrap<Geometry>(info.This());
  info.GetReturnValue().Set(Nan::New<Integer>(geom->this_->getCoordinateDimension()));
}

NAN_SETTER(Geometry::coordinateDimensionSetter) {
  Nan::HandleScope scope;
  Geometry *geom = Nan::ObjectWrap::Unwrap<Geometry>(info.This());

  if (!value->IsInt32()) {
    Nan::ThrowError("coordinateDimension must be an integer");
    return;
  }
  int dim = Nan::To<int64_t>(value).ToChecked();
  if (dim != 2 && dim != 3) {
    Nan::ThrowError("coordinateDimension must be 2 or 3");
    return;
  }

  geom->this_->setCoordinateDimension(dim);
}

Local<Value> Geometry::getConstructor(OGRwkbGeometryType type) {
  Nan::EscapableHandleScope scope;

  type = wkbFlatten(type);
  switch (type) {
    case wkbPoint: return scope.Escape(Nan::GetFunction(Nan::New(Point::constructor)).ToLocalChecked());
    case wkbLineString: return scope.Escape(Nan::GetFunction(Nan::New(LineString::constructor)).ToLocalChecked());
    case wkbLinearRing: return scope.Escape(Nan::GetFunction(Nan::New(LinearRing::constructor)).ToLocalChecked());
    case wkbPolygon: return scope.Escape(Nan::GetFunction(Nan::New(Polygon::constructor)).ToLocalChecked());
    case wkbGeometryCollection:
      return scope.Escape(Nan::GetFunction(Nan::New(GeometryCollection::constructor)).ToLocalChecked());
    case wkbMultiPoint: return scope.Escape(Nan::GetFunction(Nan::New(MultiPoint::constructor)).ToLocalChecked());
    case wkbMultiLineString:
      return scope.Escape(Nan::GetFunction(Nan::New(MultiLineString::constructor)).ToLocalChecked());
    case wkbMultiPolygon: return scope.Escape(Nan::GetFunction(Nan::New(MultiPolygon::constructor)).ToLocalChecked());
    default: return scope.Escape(Nan::Null());
  }
}

/**
 * Returns the Geometry subclass that matches the
 * given WKB geometry type.
 *
 * @static
 * @method getConstructor
 * @param {Integer} type WKB geometry type ({{#crossLink "Constants
 * (wkbGeometryType)"}}available options{{/crossLink}})
 * @return Function
 */
NAN_METHOD(Geometry::getConstructor) {
  Nan::HandleScope scope;
  OGRwkbGeometryType type;
  NODE_ARG_ENUM(0, "wkbType", OGRwkbGeometryType, type);
  info.GetReturnValue().Set(getConstructor(type));
}

/**
 * Returns the Geometry subclass name that matches the
 * given WKB geometry type.
 *
 * @static
 * @method getName
 * @param {Integer} type WKB geometry type ({{#crossLink "Constants
 * (wkbGeometryType)"}}available options{{/crossLink}})
 * @return String
 */
NAN_METHOD(Geometry::getName) {
  Nan::HandleScope scope;
  OGRwkbGeometryType type;
  NODE_ARG_ENUM(0, "wkbType", OGRwkbGeometryType, type);
  info.GetReturnValue().Set(SafeString::New(OGRGeometryTypeToName(type)));
}

} // namespace node_gdal
