#include "rasterband_pixels.hpp"
#include "../gdal_common.hpp"
#include "../gdal_rasterband.hpp"
#include "../async/async_rasterio.hpp"
#include "../utils/typed_array.hpp"

#include <sstream>

namespace node_gdal {

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

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

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

  Nan::SetPrototypeMethod(lcons, "toString", toString);
  Nan::SetPrototypeMethod(lcons, "get", get);
  Nan::SetPrototypeMethod(lcons, "set", set);
  Nan::SetPrototypeMethod(lcons, "read", read);
  Nan::SetPrototypeMethod(lcons, "readAsync", readAsync);
  Nan::SetPrototypeMethod(lcons, "write", write);
  Nan::SetPrototypeMethod(lcons, "writeAsync", writeAsync);
  Nan::SetPrototypeMethod(lcons, "readBlock", readBlock);
  Nan::SetPrototypeMethod(lcons, "writeBlock", writeBlock);

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

  constructor.Reset(lcons);
}

RasterBandPixels::RasterBandPixels() : Nan::ObjectWrap() {
}

RasterBandPixels::~RasterBandPixels() {
}

RasterBand *RasterBandPixels::parent(const Nan::FunctionCallbackInfo<v8::Value> &info) {
  Local<Object> parent =
    Nan::GetPrivate(info.This(), Nan::New("parent_").ToLocalChecked()).ToLocalChecked().As<Object>();
  RasterBand *band = Nan::ObjectWrap::Unwrap<RasterBand>(parent);
  if (!band->isAlive()) {
    Nan::ThrowError("RasterBand object has already been destroyed");
    return nullptr;
  }
  return band;
}

/**
 * A representation of a {{#crossLink
 * "gdal.RasterBand"}}RasterBand{{/crossLink}}'s pixels.
 *
 *
 * Note: Typed arrays should be created with an external ArrayBuffer for
 * versions of node >= 0.11
 * ```
 * var n = 16*16;
 * var data = new Float32Array(new ArrayBuffer(n*4));
 * //read data into the existing array
 * band.pixels.read(0,0,16,16,data);```
 *
 * @class gdal.RasterBandPixels
 */
NAN_METHOD(RasterBandPixels::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();
    RasterBandPixels *f = static_cast<RasterBandPixels *>(ptr);
    f->Wrap(info.This());
    info.GetReturnValue().Set(info.This());
    return;
  } else {
    Nan::ThrowError("Cannot create RasterBandPixels directly");
    return;
  }
}

Local<Value> RasterBandPixels::New(Local<Value> band_obj) {
  Nan::EscapableHandleScope scope;

  RasterBandPixels *wrapped = new RasterBandPixels();

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

  return scope.Escape(obj);
}

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

/**
 * Returns the value at the x, y coordinate.
 *
 * @method get
 * @param {Integer} x
 * @param {Integer} y
 * @return {Number}
 */
NAN_METHOD(RasterBandPixels::get) {
  Nan::HandleScope scope;

  RasterBand *band;
  if ((band = parent(info)) == nullptr) return;

  int x, y;
  double val;

  NODE_ARG_INT(0, "x", x);
  NODE_ARG_INT(1, "y", y);

  uv_mutex_lock(band->async_lock);
  CPLErr err = band->get()->RasterIO(GF_Read, x, y, 1, 1, &val, 1, 1, GDT_Float64, 0, 0);
  uv_mutex_unlock(band->async_lock);
  if (err) {
    NODE_THROW_CPLERR(err);
    return;
  }

  info.GetReturnValue().Set(Nan::New<Number>(val));
}

/**
 * Sets the value at the x, y coordinate.
 *
 * @method set
 * @param {Integer} x
 * @param {Integer} y
 * @param {Number} value
 */
NAN_METHOD(RasterBandPixels::set) {
  Nan::HandleScope scope;

  RasterBand *band;
  if ((band = parent(info)) == nullptr) return;

  int x, y;
  double val;

  NODE_ARG_INT(0, "x", x);
  NODE_ARG_INT(1, "y", y);
  NODE_ARG_DOUBLE(2, "val", val);

  uv_mutex_lock(band->async_lock);
  CPLErr err = band->get()->RasterIO(GF_Write, x, y, 1, 1, &val, 1, 1, GDT_Float64, 0, 0);
  uv_mutex_unlock(band->async_lock);
  if (err) {
    NODE_THROW_CPLERR(err);
    return;
  }

  return;
}

/**
 * Low level read for both synchronous and asynchronous reading.
 */
void RasterBandPixels::_do_read(const Nan::FunctionCallbackInfo<v8::Value> &info, bool async) {
  Nan::HandleScope scope;

  RasterBand *band;
  if ((band = parent(info)) == nullptr) return;

  int x, y, w, h;
  int buffer_w, buffer_h;
  int bytes_per_pixel;
  int pixel_space, line_space;
  int size, length, min_size, min_length;
  void *data;
  Local<Value> array;
  Local<Object> obj;
  GDALDataType type;

  NODE_ARG_INT(0, "x_offset", x);
  NODE_ARG_INT(1, "y_offset", y);
  NODE_ARG_INT(2, "x_size", w);
  NODE_ARG_INT(3, "y_size", h);

  std::string type_name = "";

  buffer_w = w;
  buffer_h = h;
  type = band->get()->GetRasterDataType();
  NODE_ARG_INT_OPT(5, "buffer_width", buffer_w);
  NODE_ARG_INT_OPT(6, "buffer_height", buffer_h);
  NODE_ARG_OPT_STR(7, "data_type", type_name);
  if (!type_name.empty()) { type = GDALGetDataTypeByName(type_name.c_str()); }

  if (!info[4]->IsUndefined() && !info[4]->IsNull()) {
    NODE_ARG_OBJECT(4, "data", obj);
    type = TypedArray::Identify(obj);
    if (type == GDT_Unknown) {
      Nan::ThrowError("Invalid array");
      return;
    }
  }

  bytes_per_pixel = GDALGetDataTypeSize(type) / 8;
  pixel_space = bytes_per_pixel;
  NODE_ARG_INT_OPT(8, "pixel_space", pixel_space);
  line_space = pixel_space * buffer_w;
  NODE_ARG_INT_OPT(9, "line_space", line_space);

  if (pixel_space < bytes_per_pixel) {
    Nan::ThrowError("pixel_space must be greater than or equal to size of data_type");
    return;
  }
  if (line_space < pixel_space * buffer_w) {
    Nan::ThrowError("line_space must be greater than or equal to pixel_space * buffer_w");
    return;
  }

  size = line_space * buffer_h;                      // bytes
  min_size = size - (pixel_space - bytes_per_pixel); // subtract away padding on last
                                                     // pixel that wont be written
  length = (size + bytes_per_pixel - 1) / bytes_per_pixel;
  min_length = (min_size + bytes_per_pixel - 1) / bytes_per_pixel;

  // create array if no array was passed
  if (obj.IsEmpty()) {
    array = TypedArray::New(type, length);
    if (array.IsEmpty() || !array->IsObject()) {
      return; // TypedArray::New threw an error
    }
    obj = array.As<Object>();
  }

  data = TypedArray::Validate(obj, type, min_length);
  if (!data) {
    return; // TypedArray::Validate threw an error
  }

  if (async) {
    Nan::Callback *callback;
    NODE_ARG_CB(10, "callback", callback);
    Nan::AsyncQueueWorker(new AsyncRasterIO(
      callback, band, GF_Read, x, y, w, h, &obj, data, buffer_w, buffer_h, type, pixel_space, line_space));
  } else {
    uv_mutex_lock(band->async_lock);
    CPLErr err = band->get()->RasterIO(GF_Read, x, y, w, h, data, buffer_w, buffer_h, type, pixel_space, line_space);
    uv_mutex_unlock(band->async_lock);
    if (err) {
      NODE_THROW_CPLERR(err);
      return;
    }
    info.GetReturnValue().Set(obj);
  }
}

/**
 * Reads a region of pixels.
 *
 * @method read
 * @throws Error
 * @param {Integer} x
 * @param {Integer} y
 * @param {Integer} width
 * @param {Integer} height
 * @param {TypedArray} [data] The
 * [TypedArray](https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView#Typed_array_subclasses)
 * to put the data in. A new array is created if not given.
 * @param {Object} [options]
 * @param {Integer} [options.buffer_width=x_size]
 * @param {Integer} [options.buffer_height=y_size]
 * @param {String} [options.data_type] See {{#crossLink "Constants (GDT)"}}GDT
 * constants{{/crossLink}}.
 * @param {Integer} [options.pixel_space]
 * @param {Integer} [options.line_space]
 * @return {TypedArray} A
 * [TypedArray](https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView#Typed_array_subclasses)
 * of values.
 */
NAN_METHOD(RasterBandPixels::read) {
  RasterBandPixels::_do_read(info, false);
}

/**
 * Asynchronously reads a region of pixels.
 * If the last parameter is a callback, then this callback is called on completion and undefined is returned.
 * All optional parameters before the callback can be omitted so the callback parameter can be at any position as long
 * as it is the last parameter. Otherwise the function returns a Promise resolved with the result.
 *
 * @method readAsync
 * @param {Integer} x
 * @param {Integer} y
 * @param {Integer} width
 * @param {Integer} height
 * @param {TypedArray} [data] The
 * [TypedArray](https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView#Typed_array_subclasses)
 * to put the data in. A new array is created if not given.
 * @param {Object} [options]
 * @param {Integer} [options.buffer_width=x_size]
 * @param {Integer} [options.buffer_height=y_size]
 * @param {String} [options.data_type] See {{#crossLink "Constants (GDT)"}}GDT
 * constants{{/crossLink}}.
 * @param {Integer} [options.pixel_space]
 * @param {Integer} [options.line_space]
 * @param {requestCallback} [callback] Promisifiable callback, always the last parameter, can be specified even if
 * certain optional parameters are omitted
 * @return {TypedArray} A
 * [TypedArray](https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView#Typed_array_subclasses)
 * of values.
 */
NAN_METHOD(RasterBandPixels::readAsync) {
  RasterBandPixels::_do_read(info, true);
}

/**
 * Low level read for both synchronous and asynchronous writing.
 */
void RasterBandPixels::_do_write(const Nan::FunctionCallbackInfo<v8::Value> &info, bool async) {
  Nan::HandleScope scope;

  RasterBand *band;
  if ((band = parent(info)) == nullptr) return;

  int x, y, w, h;
  int buffer_w, buffer_h;
  int bytes_per_pixel;
  int pixel_space, line_space;
  int size, min_size, min_length;
  void *data;
  Local<Object> passed_array;
  GDALDataType type;

  NODE_ARG_INT(0, "x_offset", x);
  NODE_ARG_INT(1, "y_offset", y);
  NODE_ARG_INT(2, "x_size", w);
  NODE_ARG_INT(3, "y_size", h);
  NODE_ARG_OBJECT(4, "data", passed_array);

  buffer_w = w;
  buffer_h = h;
  NODE_ARG_INT_OPT(5, "buffer_width", buffer_w);
  NODE_ARG_INT_OPT(6, "buffer_height", buffer_h);

  type = TypedArray::Identify(passed_array);
  if (type == GDT_Unknown) {
    Nan::ThrowError("Invalid array");
    return;
  }

  bytes_per_pixel = GDALGetDataTypeSize(type) / 8;
  pixel_space = bytes_per_pixel;
  NODE_ARG_INT_OPT(7, "pixel_space", pixel_space);
  line_space = pixel_space * buffer_w;
  NODE_ARG_INT_OPT(8, "line_space", line_space);

  size = line_space * buffer_h;                      // bytes
  min_size = size - (pixel_space - bytes_per_pixel); // subtract away padding on last pixel that wont be read
  min_length = (min_size + bytes_per_pixel - 1) / bytes_per_pixel;

  if (pixel_space < bytes_per_pixel) {
    Nan::ThrowError("pixel_space must be greater than or equal to size of data_type");
    return;
  }
  if (line_space < pixel_space * buffer_w) {
    Nan::ThrowError("line_space must be greater than or equal to pixel_space * buffer_w");
    return;
  }

  data = TypedArray::Validate(passed_array, type, min_length);
  if (!data) {
    return; // TypedArray::Validate threw an error
  }

  if (async) {
    Nan::Callback *callback;
    NODE_ARG_CB(9, "callback", callback);
    Nan::AsyncQueueWorker(new AsyncRasterIO(
      callback, band, GF_Read, x, y, w, h, &passed_array, data, buffer_w, buffer_h, type, pixel_space, line_space));
  } else {
    uv_mutex_lock(band->async_lock);
    CPLErr err = band->get()->RasterIO(GF_Write, x, y, w, h, data, buffer_w, buffer_h, type, pixel_space, line_space);
    uv_mutex_unlock(band->async_lock);
    if (err) {
      NODE_THROW_CPLERR(err);
      return;
    }
  }

  return;
}

/**
 * Writes a region of pixels.
 *
 * @method write
 * @throws Error
 * @param {Integer} x
 * @param {Integer} y
 * @param {Integer} width
 * @param {Integer} height
 * @param {TypedArray} data The
 * [TypedArray](https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView#Typed_array_subclasses)
 * to write to the band.
 * @param {Object} [options]
 * @param {Integer} [options.buffer_width=x_size]
 * @param {Integer} [options.buffer_height=y_size]
 * @param {Integer} [options.pixel_space]
 * @param {Integer} [options.line_space]
 */
NAN_METHOD(RasterBandPixels::write) {
  _do_write(info, false);
}

/**
 * Asynchronously writes a region of pixels.
 * If the last parameter is a callback, then this callback is called on completion and undefined is returned.
 * All optional parameters before the callback can be omitted so the callback parameter can be at any position as long
 as it is the last parameter.
 * Otherwise the function returns a Promise resolved with the result.
 *
 * @method writeAsync
 * @param {Integer} x
 * @param {Integer} y
 * @param {Integer} width
 * @param {Integer} height
 * @param {TypedArray} data The
 * [TypedArray](https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView#Typed_array_subclasses)
 * to write to the band.
 * @param {Object} [options]
 * @param {Integer} [options.buffer_width=x_size]
 * @param {Integer} [options.buffer_height=y_size]
 * @param {Integer} [options.pixel_space]
 * @param {Integer} [options.line_space]
 * @param {requestCallback} [callback] Promisifiable callback, always the last parameter, can be specified even if
 * certain optional parameters are omitted
 */
NAN_METHOD(RasterBandPixels::writeAsync) {
  _do_write(info, true);
}

/**
 * Reads a block of pixels.
 *
 * @method readBlock
 * @throws Error
 * @param {Integer} x
 * @param {Integer} y
 * @param {TypedArray} [data] The
 * [TypedArray](https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView#Typed_array_subclasses)
 * to put the data in. A new array is created if not given.
 * @return {TypedArray} A
 * [TypedArray](https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView#Typed_array_subclasses)
 * of values.
 */
NAN_METHOD(RasterBandPixels::readBlock) {
  Nan::HandleScope scope;

  RasterBand *band;
  if ((band = parent(info)) == nullptr) return;

  int x, y, w = 0, h = 0;
  NODE_ARG_INT(0, "block_x_offset", x);
  NODE_ARG_INT(1, "block_y_offset", y);

  band->get()->GetBlockSize(&w, &h);

  GDALDataType type = band->get()->GetRasterDataType();

  Local<Value> array;
  Local<Object> obj;

  if (info.Length() == 3 && !info[2]->IsUndefined() && !info[2]->IsNull()) {
    NODE_ARG_OBJECT(2, "data", obj);
    array = obj;
  } else {
    array = TypedArray::New(type, w * h);
    if (array.IsEmpty() || !array->IsObject()) {
      return; // TypedArray::New threw an error
    }
    obj = array.As<Object>();
  }

  void *data = TypedArray::Validate(obj, type, w * h);
  if (!data) {
    return; // TypedArray::Validate threw an error
  }

  uv_mutex_lock(band->async_lock);
  CPLErr err = band->get()->ReadBlock(x, y, data);
  uv_mutex_unlock(band->async_lock);
  if (err) {
    NODE_THROW_CPLERR(err);
    return;
  }

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

/**
 * Writes a block of pixels.
 *
 * @method writeBlock
 * @throws Error
 * @param {Integer} x
 * @param {Integer} y
 * @param {TypedArray} data The
 * [TypedArray](https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView#Typed_array_subclasses)
 * of values to write to the band.
 */
NAN_METHOD(RasterBandPixels::writeBlock) {
  Nan::HandleScope scope;

  RasterBand *band;
  if ((band = parent(info)) == nullptr) return;

  int x, y, w = 0, h = 0;

  band->get()->GetBlockSize(&w, &h);

  NODE_ARG_INT(0, "block_x_offset", x);
  NODE_ARG_INT(1, "block_y_offset", y);

  Local<Object> obj;
  NODE_ARG_OBJECT(2, "data", obj);

  // validate array
  void *data = TypedArray::Validate(obj, band->get()->GetRasterDataType(), w * h);
  if (!data) {
    return; // TypedArray::Validate threw an error
  }

  uv_mutex_lock(band->async_lock);
  CPLErr err = band->get()->WriteBlock(x, y, data);
  uv_mutex_unlock(band->async_lock);

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

  return;
}

} // namespace node_gdal
