#include "mapnik_datasource.hpp"
#include "mapnik_featureset.hpp"
#include "utils.hpp"
#include "ds_emitter.hpp"

// mapnik
#include <mapnik/attribute_descriptor.hpp>  // for attribute_descriptor
#include <mapnik/box2d.hpp>             // for box2d
#include <mapnik/datasource.hpp>        // for datasource, datasource_ptr, etc
#include <mapnik/datasource_cache.hpp>  // for datasource_cache
#include <mapnik/feature_layer_desc.hpp>  // for layer_descriptor
#include <mapnik/params.hpp>            // for parameters
#include <mapnik/query.hpp>             // for query

// stl
#include <exception>
#include <vector>

Nan::Persistent<v8::FunctionTemplate> Datasource::constructor;

/**
 * **`mapnik.Datasource`**
 *
 * A Datasource object. This is the connector from Mapnik to any kind
 * of file, network, or database source of geographical data.
 *
 * @class Datasource
 */
void Datasource::Initialize(v8::Local<v8::Object> target) {

    Nan::HandleScope scope;

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

    // methods
    Nan::SetPrototypeMethod(lcons, "parameters", parameters);
    Nan::SetPrototypeMethod(lcons, "describe", describe);
    Nan::SetPrototypeMethod(lcons, "featureset", featureset);
    Nan::SetPrototypeMethod(lcons, "extent", extent);
    Nan::SetPrototypeMethod(lcons, "fields", fields);

    target->Set(Nan::New("Datasource").ToLocalChecked(), lcons->GetFunction());
    constructor.Reset(lcons);
}

Datasource::Datasource() :
    Nan::ObjectWrap(),
    datasource_() {}

Datasource::~Datasource()
{
}

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

    if (info[0]->IsExternal())
    {
        v8::Local<v8::External> ext = info[0].As<v8::External>();
        void* ptr = ext->Value();
        Datasource* d =  static_cast<Datasource*>(ptr);
        if (d->datasource_->type() == mapnik::datasource::Raster)
        {
            info.This()->Set(Nan::New("type").ToLocalChecked(),
                             Nan::New("raster").ToLocalChecked());
        }
        else
        {
            info.This()->Set(Nan::New("type").ToLocalChecked(),
                             Nan::New("vector").ToLocalChecked());
        }
        d->Wrap(info.This());
        info.GetReturnValue().Set(info.This());
        return;
    }
    if (info.Length() != 1)
    {
        Nan::ThrowTypeError("accepts only one argument, an object of key:value datasource options");
        return;
    }

    if (!info[0]->IsObject())
    {
        Nan::ThrowTypeError("Must provide an object, eg {type: 'shape', file : 'world.shp'}");
        return;
    }

    v8::Local<v8::Object> options = info[0].As<v8::Object>();

    mapnik::parameters params;
    v8::Local<v8::Array> names = options->GetPropertyNames();
    unsigned int i = 0;
    unsigned int a_length = names->Length();
    while (i < a_length) {
        v8::Local<v8::Value> name = names->Get(i)->ToString();
        v8::Local<v8::Value> value = options->Get(name);
        // TODO - don't treat everything as strings
        params[TOSTR(name)] = const_cast<char const*>(TOSTR(value));
        i++;
    }

    mapnik::datasource_ptr ds;
    try
    {
        ds = mapnik::datasource_cache::instance().create(params);
    }
    catch (std::exception const& ex)
    {
        Nan::ThrowError(ex.what());
        return;
    }

    if (ds)
    {
        if (ds->type() == mapnik::datasource::Raster)
        {
            info.This()->Set(Nan::New("type").ToLocalChecked(),
                             Nan::New("raster").ToLocalChecked());
        }
        else
        {
            info.This()->Set(Nan::New("type").ToLocalChecked(),
                             Nan::New("vector").ToLocalChecked());
        }
        Datasource* d = new Datasource();
        d->Wrap(info.This());
        d->datasource_ = ds;
        info.GetReturnValue().Set(info.This());
        return;
    }
    // Not sure this point could ever be reached, because if a ds is created,
    // even if it is an empty or bad dataset the pointer will still exist
    /* LCOV_EXCL_START */
    return;
    /* LCOV_EXCL_STOP */
}

v8::Local<v8::Value> Datasource::NewInstance(mapnik::datasource_ptr ds_ptr) {
    Nan::EscapableHandleScope scope;
    Datasource* d = new Datasource();
    d->datasource_ = ds_ptr;
    v8::Local<v8::Value> ext = Nan::New<v8::External>(d);
    Nan::MaybeLocal<v8::Object> maybe_local = Nan::NewInstance(Nan::New(constructor)->GetFunction(), 1, &ext);
    if (maybe_local.IsEmpty()) Nan::ThrowError("Could not create new Datasource instance");
    return scope.Escape(maybe_local.ToLocalChecked());
}

NAN_METHOD(Datasource::parameters)
{
    Datasource* d = Nan::ObjectWrap::Unwrap<Datasource>(info.This());
    v8::Local<v8::Object> ds = Nan::New<v8::Object>();
    mapnik::parameters::const_iterator it = d->datasource_->params().begin();
    mapnik::parameters::const_iterator end = d->datasource_->params().end();
    for (; it != end; ++it)
    {
        node_mapnik::params_to_object(ds, it->first, it->second);
    }
    info.GetReturnValue().Set(ds);
}

/**
 * Get the Datasource's extent
 *
 * @name extent
 * @memberof Datasource
 * @instance
 * @returns {Array<number>} extent [minx, miny, maxx, maxy] order feature extent.
 */
NAN_METHOD(Datasource::extent)
{
    Datasource* d = Nan::ObjectWrap::Unwrap<Datasource>(info.Holder());
    mapnik::box2d<double> e;
    try
    {
        e = d->datasource_->envelope();
    }
    catch (std::exception const& ex)
    {
        // The only time this could possibly throw is situations
        // where a plugin dynamically calculated extent such as
        // postgis plugin. Therefore this makes this difficult
        // to add to testing. Therefore marking it with exclusion
        /* LCOV_EXCL_START */
        Nan::ThrowError(ex.what());
        return;
        /* LCOV_EXCL_STOP */
    }

    v8::Local<v8::Array> a = Nan::New<v8::Array>(4);
    a->Set(0, Nan::New<v8::Number>(e.minx()));
    a->Set(1, Nan::New<v8::Number>(e.miny()));
    a->Set(2, Nan::New<v8::Number>(e.maxx()));
    a->Set(3, Nan::New<v8::Number>(e.maxy()));
    info.GetReturnValue().Set(a);
}

/**
 * Describe the datasource's contents and type.
 *
 * @name describe
 * @memberof Datasource
 * @instance
 * @returns {Object} description: an object with type, fields, encoding,
 * geometry_type, and proj4 code
 */
NAN_METHOD(Datasource::describe)
{
    Datasource* d = Nan::ObjectWrap::Unwrap<Datasource>(info.Holder());
    v8::Local<v8::Object> description = Nan::New<v8::Object>();
    try
    {
        node_mapnik::describe_datasource(description,d->datasource_);
    }
    catch (std::exception const& ex)
    {
        // The only time this could possibly throw is situations
        // where a plugin dynamically calculated extent such as
        // postgis plugin. Therefore this makes this difficult
        // to add to testing. Therefore marking it with exclusion
        /* LCOV_EXCL_START */
        Nan::ThrowError(ex.what());
        return;
        /* LCOV_EXCL_STOP */
    }

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

/**
 * Get features from this dataset using an iterator.
 *
 * @name featureset
 * @memberof Datasource
 * @instance
 * @param {Object} [options]
 * @param {Array<number>} [options.extent=[minx,miny,maxx,maxy]]
 * @returns {Object} an iterator with a `.next()` method that returns
 * features from a dataset.
 * @example
 * var features = [];
 * var featureset = ds.featureset();
 * var feature;
 * while ((feature = featureset.next())) {
 *     features.push(feature);
 * }
 */
NAN_METHOD(Datasource::featureset)
{
    Datasource* ds = Nan::ObjectWrap::Unwrap<Datasource>(info.Holder());
    mapnik::box2d<double> extent = ds->datasource_->envelope();
    if (info.Length() > 0)
    {
        // options object
        if (!info[0]->IsObject())
        {
            Nan::ThrowTypeError("optional second argument must be an options object");
            return;
        }
        v8::Local<v8::Object> options = info[0]->ToObject();
        if (options->Has(Nan::New("extent").ToLocalChecked()))
        {
            v8::Local<v8::Value> extent_opt = options->Get(Nan::New("extent").ToLocalChecked());
            if (!extent_opt->IsArray())
            {
                Nan::ThrowTypeError("extent value must be an array of [minx,miny,maxx,maxy]");
                return;
            }
            v8::Local<v8::Array> bbox = extent_opt.As<v8::Array>();
            auto len = bbox->Length();
            if (!(len == 4))
            {
                Nan::ThrowTypeError("extent value must be an array of [minx,miny,maxx,maxy]");
                return;
            }
            v8::Local<v8::Value> minx = bbox->Get(0);
            v8::Local<v8::Value> miny = bbox->Get(1);
            v8::Local<v8::Value> maxx = bbox->Get(2);
            v8::Local<v8::Value> maxy = bbox->Get(3);
            if (!minx->IsNumber() || !miny->IsNumber() || !maxx->IsNumber() || !maxy->IsNumber())
            {
                Nan::ThrowError("max_extent [minx,miny,maxx,maxy] must be numbers");
                return;
            }
            extent = mapnik::box2d<double>(minx->NumberValue(),miny->NumberValue(),
                                           maxx->NumberValue(),maxy->NumberValue());
        }
    }

    mapnik::featureset_ptr fs;
    try
    {
        mapnik::query q(extent);
        mapnik::layer_descriptor ld = ds->datasource_->get_descriptor();
        auto const& desc = ld.get_descriptors();
        for (auto const& attr_info : desc)
        {
            q.add_property_name(attr_info.get_name());
        }

        fs = ds->datasource_->features(q);
    }
    catch (std::exception const& ex)
    {
        // The only time this could possibly throw is situations
        // where a plugin dynamically calculated extent such as
        // postgis plugin. Therefore this makes this difficult
        // to add to testing. Therefore marking it with exclusion
        /* LCOV_EXCL_START */
        Nan::ThrowError(ex.what());
        return;
        /* LCOV_EXCL_STOP */
    }

    if (fs && mapnik::is_valid(fs))
    {
        info.GetReturnValue().Set(Featureset::NewInstance(fs));
    }
    // This should never be able to be reached
    /* LCOV_EXCL_START */
    return;
    /* LCOV_EXCL_STOP */
}


/**
 * Get only the fields metadata from a dataset.
 *
 * @name fields
 * @memberof Datasource
 * @instance
 * @returns {Object} an object that maps from a field name to it type
 * @example
 * var fields = ds.fields();
 * // {
 * //     FIPS: 'String',
 * //     ISO2: 'String',
 * //     ISO3: 'String',
 * //     UN: 'Number',
 * //     NAME: 'String',
 * //     AREA: 'Number',
 * //     POP2005: 'Number',
 * //     REGION: 'Number',
 * //     SUBREGION: 'Number',
 * //     LON: 'Number',
 * //     LAT: 'Number'
 * // }
 */
NAN_METHOD(Datasource::fields)
{
    Datasource* d = Nan::ObjectWrap::Unwrap<Datasource>(info.Holder());
    v8::Local<v8::Object> fields = Nan::New<v8::Object>();
    node_mapnik::get_fields(fields,d->datasource_);
    info.GetReturnValue().Set(fields);
}
