#include "gdal_dataset.hpp"
#include "collections/dataset_bands.hpp"
#include "collections/dataset_layers.hpp"
#include "gdal_common.hpp"
#include "gdal_driver.hpp"
#include "gdal_geometry.hpp"
#include "gdal_layer.hpp"
#include "gdal_majorobject.hpp"
#include "gdal_rasterband.hpp"
#include "gdal_spatial_reference.hpp"

namespace node_gdal {

Nan::Persistent<FunctionTemplate> Dataset::constructor;
ObjectCache<GDALDataset, Dataset> Dataset::dataset_cache;
#if GDAL_VERSION_MAJOR < 2
ObjectCache<OGRDataSource, Dataset> Dataset::datasource_cache;
#endif

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

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

  Nan::SetPrototypeMethod(lcons, "toString", toString);
  Nan::SetPrototypeMethod(lcons, "setGCPs", setGCPs);
  Nan::SetPrototypeMethod(lcons, "getGCPs", getGCPs);
  Nan::SetPrototypeMethod(lcons, "getGCPProjection", getGCPProjection);
  Nan::SetPrototypeMethod(lcons, "getFileList", getFileList);
  Nan::SetPrototypeMethod(lcons, "flush", flush);
  Nan::SetPrototypeMethod(lcons, "close", close);
  Nan::SetPrototypeMethod(lcons, "getMetadata", getMetadata);
  Nan::SetPrototypeMethod(lcons, "testCapability", testCapability);
  Nan::SetPrototypeMethod(lcons, "executeSQL", executeSQL);
  Nan::SetPrototypeMethod(lcons, "buildOverviews", buildOverviews);

  ATTR_DONT_ENUM(lcons, "_uid", uidGetter, READ_ONLY_SETTER);
  ATTR(lcons, "description", descriptionGetter, READ_ONLY_SETTER);
  ATTR(lcons, "bands", bandsGetter, READ_ONLY_SETTER);
  ATTR(lcons, "layers", layersGetter, READ_ONLY_SETTER);
  ATTR(lcons, "rasterSize", rasterSizeGetter, READ_ONLY_SETTER);
  ATTR(lcons, "driver", driverGetter, READ_ONLY_SETTER);
  ATTR(lcons, "srs", srsGetter, srsSetter);
  ATTR(lcons, "geoTransform", geoTransformGetter, geoTransformSetter);

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

  constructor.Reset(lcons);
}

#if GDAL_VERSION_MAJOR < 2
Dataset::Dataset(GDALDataset *ds) : Nan::ObjectWrap(), uid(0), uses_ogr(false), this_dataset(ds), this_datasource(0) {
  LOG("Created Dataset [%p]", ds);
}
Dataset::Dataset(OGRDataSource *ds) : Nan::ObjectWrap(), uid(0), uses_ogr(true), this_dataset(0), this_datasource(ds) {
  LOG("Created Datasource [%p]", ds);
}
#else
Dataset::Dataset(GDALDataset *ds) : Nan::ObjectWrap(), uid(0), this_dataset(ds) {
  LOG("Created Dataset [%p]", ds);
}
#endif

Dataset::~Dataset() {
  // Destroy at garbage collection time if not already explicitly destroyed
  dispose();
}

void Dataset::dispose() {

#if GDAL_VERSION_MAJOR < 2

  if (this_datasource) {
    LOG("Disposing Datasource [%p]", this_datasource);

    ptr_manager.dispose(uid);

    LOG("Disposed Datasource [%p]", this_datasource);

    this_datasource = NULL;
  }
#endif

  if (this_dataset) {
    LOG("Disposing Dataset [%p]", this_dataset);

    ptr_manager.dispose(uid);

    LOG("Disposed Dataset [%p]", this_dataset);

    this_dataset = NULL;
  }
}

/**
 * A set of associated raster bands and/or vector layers, usually from one file.
 *
 * ```
 * // raster dataset:
 * dataset = gdal.open('file.tif');
 * bands = dataset.bands;
 *
 * // vector dataset:
 * dataset = gdal.open('file.shp');
 * layers = dataset.layers;```
 *
 * @class gdal.Dataset
 */
NAN_METHOD(Dataset::New) {
  Nan::HandleScope scope;

  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();
    Dataset *f = static_cast<Dataset *>(ptr);
    f->Wrap(info.This());

    Local<Value> bands = DatasetBands::New(info.This());
    Nan::SetPrivate(info.This(), Nan::New("bands_").ToLocalChecked(), bands);

    Local<Value> layers = DatasetLayers::New(info.This());
    Nan::SetPrivate(info.This(), Nan::New("layers_").ToLocalChecked(), layers);

    info.GetReturnValue().Set(info.This());
    return;
  } else {
    Nan::ThrowError("Cannot create dataset directly");
    return;
  }
}

Local<Value> Dataset::New(GDALDataset *raw) {
  Nan::EscapableHandleScope scope;

  if (!raw) { return scope.Escape(Nan::Null()); }
  if (dataset_cache.has(raw)) { return scope.Escape(dataset_cache.get(raw)); }

  Dataset *wrapped = new Dataset(raw);

  Local<Value> ext = Nan::New<External>(wrapped);
  Local<Object> obj =
    Nan::NewInstance(Nan::GetFunction(Nan::New(Dataset::constructor)).ToLocalChecked(), 1, &ext).ToLocalChecked();

  dataset_cache.add(raw, obj);

  /* The async locks must live outside the V8 memory management,
   * otherwise they won't be accessible from the async threads
   */
  wrapped->async_lock = new uv_mutex_t;
  uv_mutex_init(wrapped->async_lock);

  wrapped->uid = ptr_manager.add(raw, wrapped->async_lock);

  return scope.Escape(obj);
}

#if GDAL_VERSION_MAJOR < 2
Local<Value> Dataset::New(OGRDataSource *raw) {
  Nan::EscapableHandleScope scope;

  if (!raw) { return scope.Escape(Nan::Null()); }
  if (datasource_cache.has(raw)) { return scope.Escape(datasource_cache.get(raw)); }

  Dataset *wrapped = new Dataset(raw);

  Local<Value> ext = Nan::New<External>(wrapped);
  v8::Local<v8::Object> obj =
    Nan::NewInstance(Nan::GetFunction(Nan::New(Dataset::constructor)).ToLocalChecked(), 1, &ext).ToLocalChecked();

  datasource_cache.add(raw, obj);
  wrapped->async_lock = new uv_mutex_t;
  uv_mutex_init(wrapped->async_lock);
  wrapped->uid = ptr_manager.add(raw);

  return scope.Escape(obj);
}
#endif

NAN_METHOD(Dataset::toString) {
  Nan::HandleScope scope;
  info.GetReturnValue().Set(Nan::New("Dataset").ToLocalChecked());
}

/**
 * Fetch metadata.
 *
 * @method getMetadata
 * @param {string} [domain]
 * @return {Object}
 */
NAN_METHOD(Dataset::getMetadata) {
  Nan::HandleScope scope;
  Dataset *ds = Nan::ObjectWrap::Unwrap<Dataset>(info.This());

  if (!ds->isAlive()) {
    Nan::ThrowError("Dataset object has already been destroyed");
    return;
  }

#if GDAL_VERSION_MAJOR < 2
  if (ds->uses_ogr) {
    info.GetReturnValue().Set(Nan::New<Object>());
    return;
  }
#endif

  GDALDataset *raw = ds->getDataset();
  std::string domain("");
  NODE_ARG_OPT_STR(0, "domain", domain);
  uv_mutex_lock(ds->async_lock);
  info.GetReturnValue().Set(MajorObject::getMetadata(raw, domain.empty() ? NULL : domain.c_str()));
  uv_mutex_unlock(ds->async_lock);
}

/**
 * Determines if the dataset supports the indicated operation.
 *
 * @method testCapability
 * @param {string} capability (see {{#crossLink "Constants (ODsC)"}}capability
 * list{{/crossLink}})
 * @return {Boolean}
 */
NAN_METHOD(Dataset::testCapability) {
  Nan::HandleScope scope;
  Dataset *ds = Nan::ObjectWrap::Unwrap<Dataset>(info.This());

  if (!ds->isAlive()) {
    Nan::ThrowError("Dataset object has already been destroyed");
    return;
  }

#if GDAL_VERSION_MAJOR >= 2
  GDALDataset *raw = ds->getDataset();
#else
  OGRDataSource *raw = ds->getDatasource();
  if (!ds->uses_ogr) {
    info.GetReturnValue().Set(Nan::False());
    return;
  }
#endif

  std::string capability("");
  NODE_ARG_STR(0, "capability", capability);

  uv_mutex_lock(ds->async_lock);
  info.GetReturnValue().Set(Nan::New<Boolean>(raw->TestCapability(capability.c_str())));
  uv_mutex_unlock(ds->async_lock);
}

/**
 * Get output projection for GCPs.
 *
 * @method getGCPProjection
 * @return {String}
 */
NAN_METHOD(Dataset::getGCPProjection) {
  Nan::HandleScope scope;
  Dataset *ds = Nan::ObjectWrap::Unwrap<Dataset>(info.This());

  if (!ds->isAlive()) {
    Nan::ThrowError("Dataset object has already been destroyed");
    return;
  }

#if GDAL_VERSION_MAJOR < 2
  if (ds->uses_ogr) {
    info.GetReturnValue().Set(Nan::Null());
    return;
  }
#endif

  GDALDataset *raw = ds->getDataset();
  uv_mutex_lock(ds->async_lock);
  info.GetReturnValue().Set(SafeString::New(raw->GetGCPProjection()));
  uv_mutex_unlock(ds->async_lock);
}

/**
 * Closes the dataset to further operations.
 *
 * @method close
 */
NAN_METHOD(Dataset::close) {
  Dataset *ds = Nan::ObjectWrap::Unwrap<Dataset>(info.This());

  if (!ds->isAlive()) {
    Nan::ThrowError("Dataset object has already been destroyed");
    return;
  }

  ds->dispose();

  return;
}

/**
 * Flushes all changes to disk.
 *
 * @throws Error
 * @method flush
 */
NAN_METHOD(Dataset::flush) {
  Nan::HandleScope scope;
  Dataset *ds = Nan::ObjectWrap::Unwrap<Dataset>(info.This());

  if (!ds->isAlive()) {
    Nan::ThrowError("Dataset object has already been destroyed");
    return;
  }

#if GDAL_VERSION_MAJOR < 2
  if (ds->uses_ogr) {
    OGRDataSource *raw = ds->getDatasource();
    OGRErr err = raw->SyncToDisk();
    if (err) {
      NODE_THROW_OGRERR(err);
      return;
    }
    return;
  }
#endif

  GDALDataset *raw = ds->getDataset();
  if (!raw) {
    Nan::ThrowError("Dataset object has already been destroyed");
    return;
  }
  uv_mutex_lock(ds->async_lock);
  raw->FlushCache();
  uv_mutex_unlock(ds->async_lock);

  return;
}

/**
 * Execute an SQL statement against the data store.
 *
 * @throws Error
 * @method executeSQL
 * @param {String} statement SQL statement to execute.
 * @param {gdal.Geometry} [spatial_filter=null] Geometry which represents a
 * spatial filter.
 * @param {String} [dialect=null] Allows control of the statement dialect. If
 * set to `null`, the OGR SQL engine will be used, except for RDBMS drivers that
 * will use their dedicated SQL engine, unless `"OGRSQL"` is explicitely passed
 * as the dialect. Starting with OGR 1.10, the `"SQLITE"` dialect can also be
 * used.
 * @return {gdal.Layer}
 */
NAN_METHOD(Dataset::executeSQL) {
  Nan::HandleScope scope;
  Dataset *ds = Nan::ObjectWrap::Unwrap<Dataset>(info.This());

  if (!ds->isAlive()) {
    Nan::ThrowError("Dataset object has already been destroyed");
    return;
  }

#if GDAL_VERSION_MAJOR >= 2
  GDALDataset *raw = ds->getDataset();
#else
  OGRDataSource *raw = ds->getDatasource();
  if (!ds->uses_ogr) {
    Nan::ThrowError("Dataset does not support executing a SQL query");
    return;
  }
#endif

  std::string sql;
  std::string sql_dialect;
  Geometry *spatial_filter = NULL;

  NODE_ARG_STR(0, "sql text", sql);
  NODE_ARG_WRAPPED_OPT(1, "spatial filter geometry", Geometry, spatial_filter);
  NODE_ARG_OPT_STR(2, "sql dialect", sql_dialect);

  uv_mutex_lock(ds->async_lock);
  OGRLayer *layer = raw->ExecuteSQL(
    sql.c_str(), spatial_filter ? spatial_filter->get() : NULL, sql_dialect.empty() ? NULL : sql_dialect.c_str());

  if (layer) {
    info.GetReturnValue().Set(Layer::New(layer, raw, true));
    uv_mutex_unlock(ds->async_lock);
    return;
  } else {
    uv_mutex_unlock(ds->async_lock);
    Nan::ThrowError("Error executing SQL");
    return;
  }
}

/**
 * Fetch files forming dataset.
 *
 * Returns a list of files believed to be part of this dataset. If it returns an
 * empty list of files it means there is believed to be no local file system
 * files associated with the dataset (for instance a virtual dataset).
 *
 * Returns an empty array for vector datasets if GDAL version is below 2.0
 *
 * @method getFileList
 * @return {String[]}
 */
NAN_METHOD(Dataset::getFileList) {
  Nan::HandleScope scope;
  Dataset *ds = Nan::ObjectWrap::Unwrap<Dataset>(info.This());

  Local<Array> results = Nan::New<Array>(0);

  if (!ds->isAlive()) {
    Nan::ThrowError("Dataset object has already been destroyed");
    return;
  }

#if GDAL_VERSION_MAJOR < 2
  if (ds->uses_ogr) {
    info.GetReturnValue().Set(results);
    return;
  }
#endif

  GDALDataset *raw = ds->getDataset();
  if (!raw) {
    Nan::ThrowError("Dataset object has already been destroyed");
    return;
  }

  uv_mutex_lock(ds->async_lock);
  char **list = raw->GetFileList();
  if (!list) {
    info.GetReturnValue().Set(results);
    uv_mutex_unlock(ds->async_lock);
    return;
  }

  int i = 0;
  while (list[i]) {
    Nan::Set(results, i, SafeString::New(list[i]));
    i++;
  }
  uv_mutex_unlock(ds->async_lock);

  CSLDestroy(list);

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

/**
 * Fetches GCPs.
 *
 * @method getGCPs
 * @return {Object[]}
 */
NAN_METHOD(Dataset::getGCPs) {
  Nan::HandleScope scope;
  Dataset *ds = Nan::ObjectWrap::Unwrap<Dataset>(info.This());

  Local<Array> results = Nan::New<Array>(0);

  if (!ds->isAlive()) {
    Nan::ThrowError("Dataset object has already been destroyed");
    return;
  }

#if GDAL_VERSION_MAJOR < 2
  if (ds->uses_ogr) {
    info.GetReturnValue().Set(results);
    return;
  }
#endif

  GDALDataset *raw = ds->getDataset();
  if (!raw) {
    Nan::ThrowError("Dataset object has already been destroyed");
    return;
  }

  uv_mutex_lock(ds->async_lock);
  int n = raw->GetGCPCount();
  const GDAL_GCP *gcps = raw->GetGCPs();

  if (!gcps) {
    info.GetReturnValue().Set(results);
    uv_mutex_unlock(ds->async_lock);
    return;
  }

  for (int i = 0; i < n; i++) {
    GDAL_GCP gcp = gcps[i];
    Local<Object> obj = Nan::New<Object>();
    Nan::Set(obj, Nan::New("pszId").ToLocalChecked(), Nan::New(gcp.pszId).ToLocalChecked());
    Nan::Set(obj, Nan::New("pszInfo").ToLocalChecked(), Nan::New(gcp.pszInfo).ToLocalChecked());
    Nan::Set(obj, Nan::New("dfGCPPixel").ToLocalChecked(), Nan::New<Number>(gcp.dfGCPPixel));
    Nan::Set(obj, Nan::New("dfGCPLine").ToLocalChecked(), Nan::New<Number>(gcp.dfGCPLine));
    Nan::Set(obj, Nan::New("dfGCPX").ToLocalChecked(), Nan::New<Number>(gcp.dfGCPX));
    Nan::Set(obj, Nan::New("dfGCPY").ToLocalChecked(), Nan::New<Number>(gcp.dfGCPY));
    Nan::Set(obj, Nan::New("dfGCPZ").ToLocalChecked(), Nan::New<Number>(gcp.dfGCPZ));
    Nan::Set(results, i, obj);
  }

  info.GetReturnValue().Set(results);
  uv_mutex_unlock(ds->async_lock);
}

/**
 * Sets GCPs.
 *
 * @throws Error
 * @method setGCPs
 * @param {Object[]} gcps
 * @param {String} projection
 */
NAN_METHOD(Dataset::setGCPs) {
  Nan::HandleScope scope;
  Dataset *ds = Nan::ObjectWrap::Unwrap<Dataset>(info.This());

  if (!ds->isAlive()) {
    Nan::ThrowError("Dataset object has already been destroyed");
    return;
  }

#if GDAL_VERSION_MAJOR < 2
  if (ds->uses_ogr) {
    Nan::ThrowError("Dataset does not support setting GCPs");
    return;
  }
#endif

  GDALDataset *raw = ds->getDataset();
  if (!raw) {
    Nan::ThrowError("Dataset object has already been destroyed");
    return;
  }

  Local<Array> gcps;
  std::string projection("");
  NODE_ARG_ARRAY(0, "gcps", gcps);
  NODE_ARG_OPT_STR(1, "projection", projection);

  GDAL_GCP *list = new GDAL_GCP[gcps->Length()];
  std::string *pszId_list = new std::string[gcps->Length()];
  std::string *pszInfo_list = new std::string[gcps->Length()];
  GDAL_GCP *gcp = list;
  for (unsigned int i = 0; i < gcps->Length(); ++i) {
    Local<Value> val = Nan::Get(gcps, i).ToLocalChecked();
    if (!val->IsObject()) {
      if (list) {
        delete[] list;
        delete[] pszId_list;
        delete[] pszInfo_list;
      }
      Nan::ThrowError("GCP array must only include objects");
      return;
    }
    Local<Object> obj = val.As<Object>();

    NODE_DOUBLE_FROM_OBJ(obj, "dfGCPPixel", gcp->dfGCPPixel);
    NODE_DOUBLE_FROM_OBJ(obj, "dfGCPLine", gcp->dfGCPLine);
    NODE_DOUBLE_FROM_OBJ(obj, "dfGCPX", gcp->dfGCPX);
    NODE_DOUBLE_FROM_OBJ(obj, "dfGCPY", gcp->dfGCPY);
    NODE_DOUBLE_FROM_OBJ_OPT(obj, "dfGCPZ", gcp->dfGCPZ);
    NODE_STR_FROM_OBJ_OPT(obj, "pszId", pszId_list[i]);
    NODE_STR_FROM_OBJ_OPT(obj, "pszInfo", pszInfo_list[i]);

    gcp->pszId = (char *)pszId_list[i].c_str();
    gcp->pszInfo = (char *)pszInfo_list[i].c_str();

    gcp++;
  }

  uv_mutex_lock(ds->async_lock);
  CPLErr err = raw->SetGCPs(gcps->Length(), list, projection.c_str());
  uv_mutex_unlock(ds->async_lock);

  if (list) {
    delete[] list;
    delete[] pszId_list;
    delete[] pszInfo_list;
  }

  if (err) {
    NODE_THROW_CPLERR(err);
    return;
  }

  return;
}

/**
 * Builds dataset overviews.
 *
 * @throws Error
 * @method buildOverviews
 * @param {String} resampling `"NEAREST"`, `"GAUSS"`, `"CUBIC"`, `"AVERAGE"`,
 * `"MODE"`, `"AVERAGE_MAGPHASE"` or `"NONE"`
 * @param {Integer[]} overviews
 * @param {Integer[]} [bands] Note: Generation of overviews in external TIFF
 * currently only supported when operating on all bands.
 */
NAN_METHOD(Dataset::buildOverviews) {
  Nan::HandleScope scope;
  Dataset *ds = Nan::ObjectWrap::Unwrap<Dataset>(info.This());

  if (!ds->isAlive()) {
    Nan::ThrowError("Dataset object has already been destroyed");
    return;
  }

#if GDAL_VERSION_MAJOR < 2
  if (ds->uses_ogr) {
    Nan::ThrowError("Dataset does not support building overviews");
    return;
  }
#endif

  GDALDataset *raw = ds->getDataset();
  std::string resampling = "";
  Local<Array> overviews;
  Local<Array> bands;

  NODE_ARG_STR(0, "resampling", resampling);
  NODE_ARG_ARRAY(1, "overviews", overviews);
  NODE_ARG_ARRAY_OPT(2, "bands", bands);

  int *o, *b = NULL;
  int n_overviews = overviews->Length();
  int i, n_bands = 0;

  o = new int[n_overviews];
  for (i = 0; i < n_overviews; i++) {
    Local<Value> val = Nan::Get(overviews, i).ToLocalChecked();
    if (!val->IsNumber()) {
      delete[] o;
      Nan::ThrowError("overviews array must only contain numbers");
      return;
    }
    o[i] = Nan::To<int32_t>(val).ToChecked();
  }

  uv_mutex_lock(ds->async_lock);
  if (!bands.IsEmpty()) {
    n_bands = bands->Length();
    b = new int[n_bands];
    for (i = 0; i < n_bands; i++) {
      Local<Value> val = Nan::Get(bands, i).ToLocalChecked();
      if (!val->IsNumber()) {
        delete[] o;
        delete[] b;
        uv_mutex_unlock(ds->async_lock);
        Nan::ThrowError("band array must only contain numbers");
        return;
      }
      b[i] = Nan::To<int32_t>(val).ToChecked();
      if (b[i] > raw->GetRasterCount() || b[i] < 1) {
        // BuildOverviews prints an error but segfaults before returning
        delete[] o;
        delete[] b;
        uv_mutex_unlock(ds->async_lock);
        Nan::ThrowError("invalid band id");
        return;
      }
    }
  }

  CPLErr err = raw->BuildOverviews(resampling.c_str(), n_overviews, o, n_bands, b, NULL, NULL);
  uv_mutex_unlock(ds->async_lock);

  delete[] o;
  if (b) delete[] b;

  if (err) {
    NODE_THROW_CPLERR(err);
    return;
  }

  return;
}

/**
 * @readOnly
 * @attribute description
 * @type String
 */
NAN_GETTER(Dataset::descriptionGetter) {
  Nan::HandleScope scope;
  Dataset *ds = Nan::ObjectWrap::Unwrap<Dataset>(info.This());

  if (!ds->isAlive()) {
    Nan::ThrowError("Dataset object has already been destroyed");
    return;
  }

#if GDAL_VERSION_MAJOR < 2
  if (ds->uses_ogr) {
    OGRDataSource *raw = ds->getDatasource();
    info.GetReturnValue().Set(SafeString::New(raw->GetName()));
    return;
  }
#endif

  GDALDataset *raw = ds->getDataset();
  if (!raw) {
    Nan::ThrowError("Dataset object has already been destroyed");
    return;
  }
  uv_mutex_lock(ds->async_lock);
  info.GetReturnValue().Set(SafeString::New(raw->GetDescription()));
  uv_mutex_unlock(ds->async_lock);
}

/**
 * Raster dimensions. An object containing `x` and `y` properties.
 *
 * @readOnly
 * @attribute rasterSize
 * @type Object
 */
NAN_GETTER(Dataset::rasterSizeGetter) {
  Nan::HandleScope scope;
  Dataset *ds = Nan::ObjectWrap::Unwrap<Dataset>(info.This());

  if (!ds->isAlive()) {
    Nan::ThrowError("Dataset object has already been destroyed");
    return;
  }

#if GDAL_VERSION_MAJOR < 2
  if (ds->uses_ogr) {
    info.GetReturnValue().Set(Nan::Null());
    return;
  }
#endif

  GDALDataset *raw = ds->getDataset();

  // GDAL 2.x will return 512x512 for vector datasets... which doesn't really make
  // sense in JS where we can return null instead of a number
  // https://github.com/OSGeo/gdal/blob/beef45c130cc2778dcc56d85aed1104a9b31f7e6/gdal/gcore/gdaldataset.cpp#L173-L174
  uv_mutex_lock(ds->async_lock);
#if GDAL_VERSION_MAJOR >= 2
  if (raw->GetDriver() == nullptr || !raw->GetDriver()->GetMetadataItem(GDAL_DCAP_RASTER)) {
    info.GetReturnValue().Set(Nan::Null());
    uv_mutex_unlock(ds->async_lock);
    return;
  }
#endif

  Local<Object> result = Nan::New<Object>();
  Nan::Set(result, Nan::New("x").ToLocalChecked(), Nan::New<Integer>(raw->GetRasterXSize()));
  Nan::Set(result, Nan::New("y").ToLocalChecked(), Nan::New<Integer>(raw->GetRasterYSize()));
  info.GetReturnValue().Set(result);
  uv_mutex_unlock(ds->async_lock);
}

/**
 * Spatial reference associated with raster dataset
 *
 * @throws Error
 * @attribute srs
 * @type {gdal.SpatialReference}
 */
NAN_GETTER(Dataset::srsGetter) {
  Nan::HandleScope scope;
  Dataset *ds = Nan::ObjectWrap::Unwrap<Dataset>(info.This());

  if (!ds->isAlive()) {
    Nan::ThrowError("Dataset object has already been destroyed");
    return;
  }

#if GDAL_VERSION_MAJOR < 2
  if (ds->uses_ogr) {
    info.GetReturnValue().Set(Nan::Null());
    return;
  }
#endif

  GDALDataset *raw = ds->getDataset();
  uv_mutex_lock(ds->async_lock);
  // get projection wkt and return null if not set
  OGRChar *wkt = (OGRChar *)raw->GetProjectionRef();
  if (*wkt == '\0') {
    uv_mutex_unlock(ds->async_lock);
    // getProjectionRef returns string of length 0 if no srs set
    info.GetReturnValue().Set(Nan::Null());
    return;
  }
  // otherwise construct and return SpatialReference from wkt
  OGRSpatialReference *srs = new OGRSpatialReference();
  int err = srs->importFromWkt(&wkt);
  uv_mutex_unlock(ds->async_lock);

  if (err) {
    NODE_THROW_OGRERR(err);
    return;
  }

  info.GetReturnValue().Set(SpatialReference::New(srs, true));
}

/**
 * An affine transform which maps pixel/line coordinates into georeferenced
 * space using the following relationship:
 *
 * @example
 * ```
 * var GT = dataset.geoTransform;
 * var Xgeo = GT[0] + Xpixel*GT[1] + Yline*GT[2];
 * var Ygeo = GT[3] + Xpixel*GT[4] + Yline*GT[5];```
 *
 * @attribute geoTransform
 * @type {Array}
 */
NAN_GETTER(Dataset::geoTransformGetter) {
  Nan::HandleScope scope;
  Dataset *ds = Nan::ObjectWrap::Unwrap<Dataset>(info.This());

  if (!ds->isAlive()) {
    Nan::ThrowError("Dataset object has already been destroyed");
    return;
  }

#if GDAL_VERSION_MAJOR < 2
  if (ds->uses_ogr) {
    info.GetReturnValue().Set(Nan::Null());
    return;
  }
#endif

  GDALDataset *raw = ds->getDataset();
  double transform[6];
  uv_mutex_lock(ds->async_lock);
  CPLErr err = raw->GetGeoTransform(transform);
  uv_mutex_unlock(ds->async_lock);
  if (err) {
    // This is mostly (always?) a sign that it has not been set
    info.GetReturnValue().Set(Nan::Null());
    return;
    // NODE_THROW_CPLERR(err);
  }

  Local<Array> result = Nan::New<Array>(6);
  Nan::Set(result, 0, Nan::New<Number>(transform[0]));
  Nan::Set(result, 1, Nan::New<Number>(transform[1]));
  Nan::Set(result, 2, Nan::New<Number>(transform[2]));
  Nan::Set(result, 3, Nan::New<Number>(transform[3]));
  Nan::Set(result, 4, Nan::New<Number>(transform[4]));
  Nan::Set(result, 5, Nan::New<Number>(transform[5]));

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

/**
 * @readOnly
 * @attribute driver
 * @type {gdal.Driver}
 */
NAN_GETTER(Dataset::driverGetter) {
  Nan::HandleScope scope;
  Dataset *ds = Nan::ObjectWrap::Unwrap<Dataset>(info.This());

  if (!ds->isAlive()) {
    Nan::ThrowError("Dataset object has already been destroyed");
    return;
  }

#if GDAL_VERSION_MAJOR < 2
  if (ds->uses_ogr) {
    OGRDataSource *raw = ds->getDatasource();
    if (raw->GetDriver() != nullptr) { info.GetReturnValue().Set(Driver::New(raw->GetDriver())); }
    return;
  }
#endif

  GDALDataset *raw = ds->getDataset();
  if (raw->GetDriver() != nullptr) { info.GetReturnValue().Set(Driver::New(raw->GetDriver())); }
}

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

  if (!ds->isAlive()) {
    Nan::ThrowError("Dataset object has already been destroyed");
    return;
  }

#if GDAL_VERSION_MAJOR < 2
  if (ds->uses_ogr) {
    Nan::ThrowError("Dataset doesnt support setting a spatial reference");
    return;
  }
#endif

  GDALDataset *raw = ds->getDataset();
  std::string wkt("");
  if (IS_WRAPPED(value, SpatialReference)) {

    SpatialReference *srs_obj = Nan::ObjectWrap::Unwrap<SpatialReference>(value.As<Object>());
    OGRSpatialReference *srs = srs_obj->get();
    // Get wkt from OGRSpatialReference
    char *str;
    if (srs->exportToWkt(&str)) {
      Nan::ThrowError("Error exporting srs to wkt");
      return;
    }
    wkt = str; // copy string
    CPLFree(str);

  } else if (!value->IsNull() && !value->IsUndefined()) {
    Nan::ThrowError("srs must be SpatialReference object");
    return;
  }

  uv_mutex_lock(ds->async_lock);
  CPLErr err = raw->SetProjection(wkt.c_str());
  uv_mutex_unlock(ds->async_lock);

  if (err) { NODE_THROW_CPLERR(err); }
}

NAN_SETTER(Dataset::geoTransformSetter) {
  Nan::HandleScope scope;
  Dataset *ds = Nan::ObjectWrap::Unwrap<Dataset>(info.This());

  if (!ds->isAlive()) {
    Nan::ThrowError("Dataset object has already been destroyed");
    return;
  }

#if GDAL_VERSION_MAJOR < 2
  if (ds->uses_ogr) {
    Nan::ThrowError("Dataset doesnt support setting a geotransform");
    return;
  }
#endif

  GDALDataset *raw = ds->getDataset();

  if (!value->IsArray()) {
    Nan::ThrowError("Transform must be an array");
    return;
  }
  Local<Array> transform = value.As<Array>();

  if (transform->Length() != 6) {
    Nan::ThrowError("Transform array must have 6 elements");
    return;
  }

  double buffer[6];
  for (int i = 0; i < 6; i++) {
    Local<Value> val = Nan::Get(transform, i).ToLocalChecked();
    if (!val->IsNumber()) {
      Nan::ThrowError("Transform array must only contain numbers");
      return;
    }
    buffer[i] = Nan::To<double>(val).ToChecked();
  }

  uv_mutex_lock(ds->async_lock);
  CPLErr err = raw->SetGeoTransform(buffer);
  uv_mutex_unlock(ds->async_lock);

  if (err) { NODE_THROW_CPLERR(err); }
}

/**
 * @readOnly
 * @attribute bands
 * @type {gdal.DatasetBands}
 */
NAN_GETTER(Dataset::bandsGetter) {
  Nan::HandleScope scope;
  info.GetReturnValue().Set(Nan::GetPrivate(info.This(), Nan::New("bands_").ToLocalChecked()).ToLocalChecked());
}

/**
 * @readOnly
 * @attribute layers
 * @type {gdal.DatasetLayers}
 */
NAN_GETTER(Dataset::layersGetter) {
  Nan::HandleScope scope;
  info.GetReturnValue().Set(Nan::GetPrivate(info.This(), Nan::New("layers_").ToLocalChecked()).ToLocalChecked());
}

NAN_GETTER(Dataset::uidGetter) {
  Nan::HandleScope scope;
  Dataset *ds = Nan::ObjectWrap::Unwrap<Dataset>(info.This());
  info.GetReturnValue().Set(Nan::New((int)ds->uid));
}

} // namespace node_gdal
