#include "utils.hpp"
#include "mapnik_feature.hpp"
#include "mapnik_geometry.hpp"

// mapnik
#include <mapnik/version.hpp>
#include <mapnik/unicode.hpp>
#include <mapnik/feature_factory.hpp>
#include <mapnik/json/feature_parser.hpp>
#include <mapnik/value_types.hpp>
#include <mapnik/util/feature_to_geojson.hpp>

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

/**
 * **`mapnik.Feature`**
 *
 * A single geographic feature, with geometry and properties. This is
 * typically derived from data by a datasource, but can be manually
 * created.
 *
 * @class Feature
 */
void Feature::Initialize(v8::Local<v8::Object> target) {

    Nan::HandleScope scope;

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

    Nan::SetPrototypeMethod(lcons, "id", id);
    Nan::SetPrototypeMethod(lcons, "extent", extent);
    Nan::SetPrototypeMethod(lcons, "attributes", attributes);
    Nan::SetPrototypeMethod(lcons, "geometry", geometry);
    Nan::SetPrototypeMethod(lcons, "toJSON", toJSON);

    Nan::SetMethod(lcons->GetFunction().As<v8::Object>(),
                    "fromJSON",
                    Feature::fromJSON);

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

Feature::Feature(mapnik::feature_ptr f) :
    Nan::ObjectWrap(),
    this_(f) {}

Feature::Feature(int id) :
    Nan::ObjectWrap(),
    this_() {
    // TODO - accept/require context object to reused
    ctx_ = std::make_shared<mapnik::context_type>();
    this_ = mapnik::feature_factory::create(ctx_,id);
}

Feature::~Feature()
{
}

NAN_METHOD(Feature::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();
        Feature* f =  static_cast<Feature*>(ptr);
        f->Wrap(info.This());
        info.GetReturnValue().Set(info.This());
        return;
    }

    // TODO - expose mapnik.Context

    if (info.Length() > 1 || info.Length() < 1 || !info[0]->IsNumber()) {
        Nan::ThrowTypeError("requires one argument: an integer feature id");
        return;
    }

    Feature* f = new Feature(info[0]->IntegerValue());
    f->Wrap(info.This());
    info.GetReturnValue().Set(info.This());
}

/**
 * @memberof Feature
 * @static
 * @name fromJSON
 * @param {string} geojson string
 *
 * Create a feature from a GeoJSON representation.
 */
NAN_METHOD(Feature::fromJSON)
{
    if (info.Length() < 1 || !info[0]->IsString()) {
        Nan::ThrowTypeError("requires one argument: a string representing a GeoJSON feature");
        return;
    }
    std::string json = TOSTR(info[0]);
    try
    {
        mapnik::feature_ptr f(mapnik::feature_factory::create(std::make_shared<mapnik::context_type>(),1));
        if (!mapnik::json::from_geojson(json,*f))
        {
            Nan::ThrowError("Failed to read GeoJSON");
            return;
        }
        Feature* feat = new Feature(f);
        v8::Local<v8::Value> ext = Nan::New<v8::External>(feat);
        Nan::MaybeLocal<v8::Object> maybe_local = Nan::NewInstance(Nan::New(constructor)->GetFunction(), 1, &ext);
        if (maybe_local.IsEmpty()) Nan::ThrowError("Could not create new Feature instance");
        else info.GetReturnValue().Set(maybe_local.ToLocalChecked());
    }
    catch (std::exception const& ex)
    {
        // no way currently for any of the above code to throw,
        // but we'll keep this try catch to protect against mapnik or v8 changing
        /* LCOV_EXCL_START */
        Nan::ThrowError(ex.what());
        return;
        /* LCOV_EXCL_STOP */
    }
}

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

/**
 * @memberof Feature
 * @name id
 * @instance
 * @returns {number} id the feature's internal id
 */
NAN_METHOD(Feature::id)
{
    Feature* fp = Nan::ObjectWrap::Unwrap<Feature>(info.Holder());
    info.GetReturnValue().Set(Nan::New<v8::Number>(fp->get()->id()));
}

/**
 * Get the feature's extent
 *
 * @name extent
 * @memberof Feature
 * @instance
 * @returns {Array<number>} extent [minx, miny, maxx, maxy] order feature extent.
 */
NAN_METHOD(Feature::extent)
{
    Feature* fp = Nan::ObjectWrap::Unwrap<Feature>(info.Holder());
    v8::Local<v8::Array> a = Nan::New<v8::Array>(4);
    mapnik::box2d<double> const& e = fp->get()->envelope();
    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);
}

/**
 * Get the feature's attributes as an object.
 *
 * @name attributes
 * @memberof Feature
 * @instance
 * @returns {Object} attributes
 */
NAN_METHOD(Feature::attributes)
{
    Feature* fp = Nan::ObjectWrap::Unwrap<Feature>(info.Holder());
    v8::Local<v8::Object> feat = Nan::New<v8::Object>();
    mapnik::feature_ptr feature = fp->get();
    if (feature)
    {
        for (auto const& attr : *feature)
        {
            feat->Set(Nan::New<v8::String>(std::get<0>(attr)).ToLocalChecked(),
                      mapnik::util::apply_visitor(node_mapnik::value_converter(), std::get<1>(attr))
                );
        }
    }
    info.GetReturnValue().Set(feat);
}


/**
 * Get the feature's attributes as a Mapnik geometry.
 *
 * @name geometry
 * @memberof Feature
 * @instance
 * @returns {mapnik.Geometry} geometry
 */
NAN_METHOD(Feature::geometry)
{
    Feature* fp = Nan::ObjectWrap::Unwrap<Feature>(info.Holder());
    info.GetReturnValue().Set(Geometry::NewInstance(fp->get()));
}

/**
 * Generate and return a GeoJSON representation of this feature
 *
 * @instance
 * @name toJSON
 * @memberof Feature
 * @returns {string} geojson Feature object in stringified GeoJSON
 */
NAN_METHOD(Feature::toJSON)
{
    Feature* fp = Nan::ObjectWrap::Unwrap<Feature>(info.Holder());
    std::string json;
    if (!mapnik::util::to_geojson(json, *(fp->get())))
    {
        /* LCOV_EXCL_START */
        Nan::ThrowError("Failed to generate GeoJSON");
        return;
        /* LCOV_EXCL_STOP */
    }
    info.GetReturnValue().Set(Nan::New<v8::String>(json).ToLocalChecked());
}
