#include "gdal_algorithms.hpp"
#include "gdal_common.hpp"
#include "gdal_layer.hpp"
#include "gdal_dataset.hpp"
#include "gdal_rasterband.hpp"
#include "utils/number_list.hpp"
#include "cpl_string.h"
#include "utils/translate_options.hpp"

namespace node_gdal {

void Algorithms::Initialize(Local<Object> target)
{
	Nan::SetMethod(target, "fillNodata", fillNodata);
	Nan::SetMethod(target, "contourGenerate", contourGenerate);
	Nan::SetMethod(target, "sieveFilter", sieveFilter);
	Nan::SetMethod(target, "checksumImage", checksumImage);
	Nan::SetMethod(target, "polygonize", polygonize);
	Nan::SetMethod(target, "translate", translate);
}

/**
 * Fill raster regions by interpolation from edges.
 *
 * @throws Error
 * @method fillNodata
 * @static
 * @for gdal
 * @param {Object} options
 * @param {gdal.RasterBand} options.src This band to be updated in-place.
 * @param {gdal.RasterBand} [options.mask] Mask band
 * @param {Number} options.searchDist The maximum distance (in pixels) that the algorithm will search out for values to interpolate.
 * @param {integer} [options.smoothingIterations=0] The number of 3x3 average filter smoothing iterations to run after the interpolation to dampen artifacts.
 */
NAN_METHOD(Algorithms::fillNodata)
{
	Nan::HandleScope scope;

	Local<Object> obj;
	RasterBand* src;
	RasterBand* mask = NULL;
	double search_dist;
	int smooth_iterations = 0;

	NODE_ARG_OBJECT(0, "options", obj);

	NODE_WRAPPED_FROM_OBJ(obj, "src", RasterBand, src);
	NODE_WRAPPED_FROM_OBJ_OPT(obj, "mask", RasterBand, mask);
	NODE_DOUBLE_FROM_OBJ(obj, "searchDist", search_dist);
	NODE_INT_FROM_OBJ_OPT(obj, "smoothIterations", smooth_iterations)

	CPLErr err = GDALFillNodata(src->get(), mask ? mask->get() : NULL, search_dist, 0, smooth_iterations, NULL, NULL, NULL);

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

	return;
}

/**
 * Create vector contours from raster DEM.
 *
 * This algorithm will generate contour vectors for the input raster band on the
 * requested set of contour levels. The vector contours are written to the passed
 * in vector layer. Also, a NODATA value may be specified to identify pixels
 * that should not be considered in contour line generation.
 *
 * @throws Error
 * @method contourGenerate
 * @static
 * @for gdal
 * @param {Object} options
 * @param {gdal.RasterBand} options.src
 * @param {gdal.Layer} options.dst
 * @param {Number} [options.offset=0] The "offset" relative to which contour intervals are applied. This is normally zero, but could be different. To generate 10m contours at 5, 15, 25, ... the offset would be 5.
 * @param {Number} [options.interval=100] The elevation interval between contours generated.
 * @param {Number[]} [options.fixedLevels] A list of fixed contour levels at which contours should be generated. Overrides interval/base options if set.
 * @param {Number} [options.nodata] The value to use as a "nodata" value. That is, a pixel value which should be ignored in generating contours as if the value of the pixel were not known.
 * @param {integer} [options.idField] A field index to indicate where a unique id should be written for each feature (contour) written.
 * @param {integer} [options.elevField] A field index to indicate where the elevation value of the contour should be written.
 */
NAN_METHOD(Algorithms::contourGenerate)
{
	Nan::HandleScope scope;

	Local<Object> obj;
	Local<Value> prop;
	RasterBand* src;
	Layer* dst;
	double interval = 100, base = 0;
	double *fixed_levels = NULL;
	DoubleList fixed_level_array;
	int n_fixed_levels = 0;
	int use_nodata = 0;
	double nodata = 0;
	int id_field = -1, elev_field = -1;

	NODE_ARG_OBJECT(0, "options", obj);

	NODE_WRAPPED_FROM_OBJ(obj, "src", RasterBand, src);
	NODE_WRAPPED_FROM_OBJ(obj, "dst", Layer, dst);
	NODE_INT_FROM_OBJ_OPT(obj, "idField", id_field);
	NODE_INT_FROM_OBJ_OPT(obj, "elevField", elev_field);
	NODE_DOUBLE_FROM_OBJ_OPT(obj, "interval", interval);
	NODE_DOUBLE_FROM_OBJ_OPT(obj, "offset", base);
	if(Nan::HasOwnProperty(obj, Nan::New("fixedLevels").ToLocalChecked()).FromMaybe(false)){
		if(fixed_level_array.parse(Nan::Get(obj, Nan::New("fixedLevels").ToLocalChecked()).ToLocalChecked())){
			return; //error parsing double list
		} else {
			fixed_levels = fixed_level_array.get();
			n_fixed_levels = fixed_level_array.length();
		}
	}
	if(Nan::HasOwnProperty(obj, Nan::New("nodata").ToLocalChecked()).FromMaybe(false)){
		prop = Nan::Get(obj, Nan::New("nodata").ToLocalChecked()).ToLocalChecked();
		if(prop->IsNumber()){
			use_nodata = 1;
			nodata = Nan::To<double>(prop).ToChecked();
		} else if(!prop->IsNull() && !prop->IsUndefined()){
			Nan::ThrowTypeError("nodata property must be a number");
		}
	}

	CPLErr err = GDALContourGenerate(src->get(), interval, base, n_fixed_levels, fixed_levels, use_nodata, nodata, dst->get(), id_field, elev_field, NULL, NULL);

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

	return;
}

/**
 * Removes small raster polygons.
 *
 * @throws Error
 * @method sieveFilter
 * @static
 * @for gdal
 * @param {Object} options
 * @param {gdal.RasterBand} options.src
 * @param {gdal.RasterBand} options.dst Output raster band. It may be the same as src band to update the source in place.
 * @param {gdal.RasterBand} [options.mask] All pixels in the mask band with a value other than zero will be considered suitable for inclusion in polygons.
 * @param {Number} options.threshold Raster polygons with sizes smaller than this will be merged into their largest neighbour.
 * @param {integer} [options.connectedness=4] Either 4 indicating that diagonal pixels are not considered directly adjacent for polygon membership purposes or 8 indicating they are.
 */
NAN_METHOD(Algorithms::sieveFilter)
{
	Nan::HandleScope scope;

	Local<Object> obj;
	RasterBand* src;
	RasterBand* dst;
	RasterBand* mask = NULL;
	int threshold;
	int connectedness = 4;

	NODE_ARG_OBJECT(0, "options", obj);

	NODE_WRAPPED_FROM_OBJ(obj, "src", RasterBand, src);
	NODE_WRAPPED_FROM_OBJ(obj, "dst", RasterBand, dst);
	NODE_WRAPPED_FROM_OBJ_OPT(obj, "mask", RasterBand, mask);
	NODE_INT_FROM_OBJ(obj, "threshold", threshold);
	NODE_INT_FROM_OBJ_OPT(obj, "connectedness", connectedness);

	if(connectedness != 4 && connectedness != 8){
		Nan::ThrowError("connectedness option must be 4 or 8");
		return;
	}

	CPLErr err = GDALSieveFilter(src->get(), mask ? mask->get() : NULL, dst->get(), threshold, connectedness, NULL, NULL, NULL);

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

	return;
}

/**
 * Compute checksum for image region.
 *
 * @throws Error
 * @method checksumImage
 * @static
 * @for gdal
 * @param {gdal.RasterBand} src
 * @param {integer} [x=0]
 * @param {integer} [y=0]
 * @param {integer} [w=src.width]
 * @param {integer} [h=src.height]
 * @return integer
 */
NAN_METHOD(Algorithms::checksumImage)
{
	Nan::HandleScope scope;

	RasterBand* src;
	int x = 0, y = 0, w, h, bandw, bandh;

	NODE_ARG_WRAPPED(0, "src", RasterBand, src);

	w = bandw = src->get()->GetXSize();
	h = bandh = src->get()->GetYSize();

	NODE_ARG_INT_OPT(1, "x", x);
	NODE_ARG_INT_OPT(2, "y", y);
	NODE_ARG_INT_OPT(3, "xSize", w);
	NODE_ARG_INT_OPT(4, "ySize", h);

	if(x < 0 || y < 0 || x >= bandw || y >= bandh){
		Nan::ThrowRangeError("offset invalid for given band");
		return;
	}
	if(w < 0 || h < 0 || w > bandw || h > bandh){
		Nan::ThrowRangeError("x and y size must be smaller than band dimensions and greater than 0");
		return;
	}
	if(x+w-1 >= bandw || y+h-1 >= bandh){
		Nan::ThrowRangeError("given range is outside bounds of given band");
		return;
	}

	int checksum = GDALChecksumImage(src->get(), x, y, w, h);

	info.GetReturnValue().Set(Nan::New<Integer>(checksum));
}

/**
 * Creates vector polygons for all connected regions of pixels in the raster
 * sharing a common pixel value. Each polygon is created with an attribute
 * indicating the pixel value of that polygon. A raster mask may also be
 * provided to determine which pixels are eligible for processing.
 *
 * @throws Error
 * @method polygonize
 * @static
 * @for gdal
 * @param {Object} options
 * @param {gdal.RasterBand} options.src
 * @param {gdal.Layer} options.dst
 * @param {gdal.RasterBand} [options.mask]
 * @param {integer} options.pixValField The attribute field index indicating the feature attribute into which the pixel value of the polygon should be written.
 * @param {integer} [options.connectedness=4] Either 4 indicating that diagonal pixels are not considered directly adjacent for polygon membership purposes or 8 indicating they are.
 * @param {Boolean} [options.useFloats=false] Use floating point buffers instead of int buffers.
 */
NAN_METHOD(Algorithms::polygonize)
{
	Nan::HandleScope scope;

	Local<Object> obj;
	RasterBand* src;
	RasterBand* mask = NULL;
	Layer* dst;
	int connectedness = 4;
	int pix_val_field = 0;
	char** papszOptions = NULL;

	NODE_ARG_OBJECT(0, "options", obj);

	NODE_WRAPPED_FROM_OBJ(obj, "src", RasterBand, src);
	NODE_WRAPPED_FROM_OBJ(obj, "dst", Layer, dst);
	NODE_WRAPPED_FROM_OBJ_OPT(obj, "mask", RasterBand, mask);
	NODE_INT_FROM_OBJ_OPT(obj, "connectedness", connectedness)
	NODE_INT_FROM_OBJ(obj, "pixValField", pix_val_field);

	if(connectedness == 8) {
		papszOptions = CSLSetNameValue(papszOptions, "8CONNECTED", "8");
	} else if (connectedness != 4) {
		Nan::ThrowError("connectedness must be 4 or 8");
		return;
	}

	CPLErr err;
	if(Nan::HasOwnProperty(obj, Nan::New("useFloats").ToLocalChecked()).FromMaybe(false) && Nan::To<bool>(Nan::Get(obj, Nan::New("useFloats").ToLocalChecked()).ToLocalChecked()).ToChecked()){
		err = GDALFPolygonize(src->get(), mask ? mask->get() : NULL, reinterpret_cast<OGRLayerH>(dst->get()), pix_val_field, papszOptions, NULL, NULL);
	} else {
		err = GDALPolygonize(src->get(), mask ? mask->get() : NULL, reinterpret_cast<OGRLayerH>(dst->get()), pix_val_field, papszOptions, NULL, NULL);
	}

	if(papszOptions) CSLDestroy(papszOptions);

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

	return;
}

/**
 * Calls gdal_translate.
 *
 * @throws Error
 * @method translate
 * @static
 * @for gdal
 * @param {Object} options
 * @param {gdal.Dataset} options.src the source dataset handle
 * @param {String} options.dst the destination dataset path
 * @param {String} [options.outputFormat] the desired output format. see ({{#crossLink "gdal.GDALDrivers"}}drivers list{{/crossLink}}) for possible options.
 * @param {gdal.Envelope} [options.projWin] selects a subwindow from the source image for copying with the corners given in georeferenced coordinates (by default expressed in the SRS of the dataset. Can be changed with projWinSRS)
 * @param {String} [options.projWinSRS] specifies the SRS in which to interpret the coordinates given with projWin. The srs_def may be any of the usual GDAL/OGR forms, complete WKT, PROJ.4, EPSG:n or a file containing the WKT. Note that this does not cause reprojection of the dataset to the specified SRS.
 * @return {gdal.Dataset} newly created dataset
 */
NAN_METHOD(Algorithms::translate)
{
	Nan::HandleScope scope;

	Local<Object> obj;
	Local<Value> prop;

	GDALDataset* src;
	std::string dst;
	TranslateOptions options;
	GDALTranslateOptions* opts;
	Dataset* res;

	NODE_ARG_OBJECT(0, "options", obj);

    if(options.parse(obj)){
        return; // error parsing options object
    } else {
        opts = options.get();
    }

	// Parse input source dataset
    if(Nan::HasOwnProperty(obj, Nan::New("src").ToLocalChecked()).FromMaybe(false)){
        prop = obj->Get(Nan::New("src").ToLocalChecked());
        if(prop->IsObject() && !prop->IsNull() && Nan::New(Dataset::constructor)->HasInstance(prop)){
            Dataset *ds = Nan::ObjectWrap::Unwrap<Dataset>(prop.As<Object>());
            src = ds->getDataset();
            if(!src){
                #if GDAL_VERSION_MAJOR < 2
                if(ds->getDatasource()) {
                    Nan::ThrowError("src dataset must be a raster dataset"); return;
                }
                #endif
                Nan::ThrowError("src dataset already closed"); return;
            }
        } else {
            Nan::ThrowTypeError("src property must be a Dataset object"); return;
        }
    } else {
        Nan::ThrowError("Translate options must include a source dataset"); return;
    }

    // Parse target destination path
    Local<String> sym = Nan::New("dst").ToLocalChecked();
    if (!Nan::HasOwnProperty(obj, sym).FromMaybe(false)){
        Nan::ThrowError("Object must contain property dst"); return;
    }
    Local<Value> val = obj->Get(sym);
    if (!val->IsString()){
        Nan::ThrowTypeError("Property dst must be a string"); return;
    }
	NODE_STR_FROM_OBJ(obj, "dst", dst);

    NODE_STR_FROM_OBJ_OPT(obj, "outputFormat", opts->pszFormat);

	// Call translate function
	GDALTranslate(dst.c_str(), src, opts, NULL);

	// Set return value
	// TODO Implement this
//	info.GetReturnValue().Set(Nan::New<Dataset>(res));

	return;
}

} //node_gdal namespace
