#include "V8Context.h"

#include <pthread.h>
#include <time.h>

#include <sstream>
#include <iostream>

#ifndef INT32_MAX
#define INT32_MAX 0x7fffffff
#define INT32_MIN (-0x7fffffff-1)
#endif

#define L(...) fprintf(stderr, ##__VA_ARGS__)

using namespace v8;
using namespace std;

int V8Context::number = 0;

v8::Isolate* isolate;
std::unique_ptr<Platform> plf;

void set_perl_error(Local<Context> ctx, const TryCatch& try_catch) {
    Handle<Message> msg = try_catch.Message();

    char message[1024];
    snprintf(
        message,
        1024,
        "%s at %s:%d:%d\n",
        *(String::Utf8Value(isolate, try_catch.Exception())),
        !msg.IsEmpty() ? *(String::Utf8Value(isolate, msg->GetScriptResourceName())) : "eval",
        !msg.IsEmpty() ? msg->GetLineNumber(ctx).FromMaybe(0) : 0,
        !msg.IsEmpty() ? msg->GetStartColumn(ctx).FromMaybe(0) : 0
    );

    sv_setpv(ERRSV, message);
    sv_utf8_upgrade(ERRSV);
}

Handle<Value>
check_perl_error() {
    if (!SvOK(ERRSV))
        return Handle<Value>();

    const char *err = SvPV_nolen(ERRSV);

    if (err && strlen(err) > 0) {
        Handle<String> error = v8::String::NewFromUtf8(isolate, err, v8::String::kNormalString);
        sv_setsv(ERRSV, &PL_sv_no);
        Handle<Value> v = isolate->ThrowException(Exception::Error(error));
        return v;
    }

    return Handle<Value>();
}

// Internally-used wrapper around coderefs
static IV
calculate_size(SV *sv) {
    return 1000;
    /*
     * There are horrible bugs in the current Devel::Size, so we can't do this
     * accurately. But if there weren't, this is how we'd do it!
    dSP;
    ENTER;
    SAVETMPS;

    PUSHMARK(SP);
    XPUSHs(sv);
    PUTBACK;
    int returned = call_pv("Devel::Size::total_size", G_SCALAR);
    if (returned != 1) {
        warn("Error calculating sv size");
        return 0;
    }

    SPAGAIN;
    IV size    = SvIV(POPs);
    PUTBACK;
    FREETMPS;
    LEAVE;

    return size;
    */
}

#define SETUP_PERL_CALL(PUSHSELF) \
    int len = args.Length(); \
\
    dSP; \
    ENTER; \
    SAVETMPS; \
\
    PUSHMARK(SP); \
\
    PUSHSELF; \
\
    for (int i = 1; i < len; i++) { \
        SV *arg = context->v82sv(args[i]); \
        mXPUSHs(arg); \
    } \
    PUTBACK;

#define CONVERT_PERL_RESULT() \
    Handle<Value> error = check_perl_error(); \
\
    if (!error.IsEmpty()) { \
        FREETMPS; \
        LEAVE; \
        return error; \
    } \
    SPAGAIN; \
\
    Handle<Value> v = context->sv2v8(POPs); \
\
    PUTBACK; \
    FREETMPS; \
    LEAVE; \
\
    return v;

void SvMap::add(Handle<Object> object, long ptr) {
    objects.insert(
        pair<int, SimpleObjectData*>(
            object->GetIdentityHash(),
            new SimpleObjectData(object, ptr)
        )
    );
}

SV* SvMap::find(Local<Context> ctx, Handle<Object> object) {
    int hash = object->GetIdentityHash();

    for (sv_map::const_iterator it = objects.find(hash); it != objects.end() && it->first == hash; it++)
        if (it->second->object->Equals(ctx, object).FromMaybe(0))
            return newRV_inc(INT2PTR(SV*, it->second->ptr));

    return NULL;
}

ObjectData::ObjectData(V8Context* context_, Handle<Object> object_, SV* sv_)
{
    context = context_;
    sv = sv_;

    Persistent<Object, CopyablePersistentTraits<Object>> persistent(isolate, object_);

    object = persistent;

    if (!sv) return;

    ptr = PTR2IV(sv);

    context->register_object(this);
}

ObjectData::~ObjectData() {
    if (context) context->remove_object(this);
    object.Reset();
}

PerlObjectData::PerlObjectData(V8Context* context_, Handle<Object> object_, SV* sv_)
    : ObjectData(context_, object_, sv_)
    , bytes(size())
{
    if (!sv)
        return;

    SvREFCNT_inc(sv);
    add_size(calculate_size(sv));
    ptr = PTR2IV(sv);

    object.SetWeak(this, PerlObjectData::destroy, v8::WeakCallbackType::kParameter);
}

size_t PerlObjectData::size() {
    return sizeof(PerlObjectData);
}

PerlObjectData::~PerlObjectData() {
    add_size(-bytes);
    SvREFCNT_dec((SV*)sv);
}

V8ObjectData::V8ObjectData(V8Context* context_, Handle<Object> object_, SV* sv_)
    : ObjectData(context_, object_, sv_)
{
    SV *iv = newSViv((IV) this);
    sv_magicext(sv, iv, PERL_MAGIC_ext, &vtable, "v8v8", 0);
    SvREFCNT_dec(iv); // refcnt is incremented by sv_magicext
}

MGVTBL V8ObjectData::vtable = {
    0,
    0,
    0,
    0,
    V8ObjectData::svt_free
};

int V8ObjectData::svt_free(pTHX_ SV* sv, MAGIC* mg) {
    delete (V8ObjectData*)SvIV(mg->mg_obj);
    return 0;
};

void PerlObjectData::destroy(const WeakCallbackInfo<PerlObjectData>& data) {
    data.GetParameter()->object.Reset();
    delete static_cast<PerlObjectData*>(data.GetParameter());
}

ObjectData* sv_object_data(SV* sv) {
    if (MAGIC *mg = mg_find(sv, PERL_MAGIC_ext)) {
        if (mg->mg_virtual == &V8ObjectData::vtable) {
            return (ObjectData*)SvIV(mg->mg_obj);
        }
    }
    return NULL;
}

class V8FunctionData : public V8ObjectData {
public:
    V8FunctionData(V8Context* context_, Handle<Object> object_, SV* sv_)
        : V8ObjectData(context_, object_, sv_)
        , returns_list(object_->Has(context_->get_local_context(), String::NewFromUtf8(isolate, "__perlReturnsList", v8::String::kNormalString)).FromMaybe(0))
    { }

    bool returns_list;
};

class PerlFunctionData : public PerlObjectData {
private:
    SV *rv;
    Local<Value> *thisWrapped;

protected:
    virtual Handle<Value> invoke(const v8::FunctionCallbackInfo<v8::Value>& args);
    virtual size_t size();

public:
    PerlFunctionData(V8Context* context_, SV *cv)
        : PerlObjectData(
              context_,
              Handle<Object>::Cast(
                  context_->make_function.Get(isolate)->Call(
                      context_->get_local_context(),
                      context_->get_local_context()->Global(),
                      1,
                      thisWrapped = new Local<Value>(External::New(isolate, this))
                  ).ToLocalChecked()
              ),
              cv
          )
       , rv(cv ? newRV_noinc(cv) : NULL)
    { }

    ~PerlFunctionData() {
        delete thisWrapped;
    }

    static void v8invoke(const v8::FunctionCallbackInfo<v8::Value>& args) {
        v8::Local<v8::External> ext = args[0].As<v8::External>();

        PerlFunctionData* data = static_cast<PerlFunctionData*>(ext->Value());

        if (data != NULL) {
            args.GetReturnValue().Set(data->invoke(args));
        }
    }
};

size_t PerlFunctionData::size() {
    return sizeof(PerlFunctionData);
}

void PerlObjectData::add_size(size_t bytes_) {
    bytes += bytes_;
    //V8::AdjustAmountOfExternalAllocatedMemory(bytes_);
}

Handle<Value>
PerlFunctionData::invoke(const v8::FunctionCallbackInfo<v8::Value>& args) {
    SETUP_PERL_CALL();
    int count = call_sv(rv, G_SCALAR | G_EVAL);
    CONVERT_PERL_RESULT();
}

class PerlMethodData : public PerlFunctionData {
private:
    string name;
    virtual Handle<Value> invoke(const v8::FunctionCallbackInfo<v8::Value>& args);
    virtual size_t size();

public:
    PerlMethodData(V8Context* context_, char* name_)
        : PerlFunctionData(context_, NULL)
        , name(name_)
    { }
};

Handle<Value>
PerlMethodData::invoke(const v8::FunctionCallbackInfo<v8::Value>& args) {
    SETUP_PERL_CALL(mXPUSHs(context->v82sv(args.This())))
    int count = call_method(name.c_str(), G_SCALAR | G_EVAL);
    CONVERT_PERL_RESULT()
}

size_t PerlMethodData::size() {
    return sizeof(PerlMethodData);
}

// V8Context class starts here

V8Context::V8Context(
    int time_limit,
    const char* flags,
    bool enable_blessing_,
    const char* bless_prefix_
)
    : time_limit_(time_limit),
      bless_prefix(bless_prefix_),
      enable_blessing(enable_blessing_)
{
    // Set flags before creating the isolate--otherwise some flags are
    // ineffective.
    V8::SetFlagsFromString(flags, strlen(flags));

    if (!isolate) {
        //v8::V8::InitializeICU();
        plf = platform::NewDefaultPlatform();
        V8::InitializePlatform(plf.get());
        V8::Initialize();

        v8::Isolate::CreateParams create_params;

        create_params.array_buffer_allocator =
            v8::ArrayBuffer::Allocator::NewDefaultAllocator();

        isolate = v8::Isolate::New(create_params);
    }

    Isolate::Scope isolate_scope(isolate);

    HandleScope handle_scope(isolate);

    v8::Local<v8::ObjectTemplate> global = v8::ObjectTemplate::New(isolate);

    v8::Local<v8::Context> context = Context::New(isolate, NULL, global);

    Persistent<Context, CopyablePersistentTraits<Context>> persistent_context(isolate, context);

    this->context = persistent_context;

    Context::Scope context_scope(context);

    Local<FunctionTemplate> tmpl = FunctionTemplate::New(isolate, PerlFunctionData::v8invoke);

    context->Global()->Set(
        v8::String::NewFromUtf8(isolate, "__perlFunctionWrapper", v8::String::kNormalString),
        tmpl->GetFunction(context).ToLocalChecked()
    );

    Handle<Script> script = Script::Compile(
        context,
        v8::String::NewFromUtf8(
            isolate,
            "(function(wrap) {"
            "    return function() {"
            "        var args = Array.prototype.slice.call(arguments);"
            "        args.unshift(wrap);"
            "        return __perlFunctionWrapper.apply(this, args)"
            "    };"
            "})",
            v8::String::kNormalString
        )
    ).ToLocalChecked();

    make_function.Reset(isolate, Handle<Function>::Cast(script->Run(context).ToLocalChecked()));

    string_wrap.Reset(isolate, String::NewFromUtf8(isolate, "wrap"));

    number++;
}

Local<Context> V8Context::get_local_context() {
    return Local<Context>::New(isolate, context);
}

void V8Context::register_object(ObjectData* data) {
    seen_perl[data->ptr] = data;

    v8::Local<v8::Private> privateKey = v8::Private::ForApi(isolate, string_wrap.Get(isolate));
    auto local = v8::Local<v8::Object>::New(isolate, data->object);
    v8::Local<v8::Context> context = isolate->GetCurrentContext();

    local->SetPrivate(context, privateKey, External::New(isolate, data));
}

void V8Context::remove_object(ObjectData* data) {
    ObjectDataMap::iterator it = seen_perl.find(data->ptr);
    if (it != seen_perl.end())
        seen_perl.erase(it);

    HandleScope scope(isolate);
    Local<Context> local_context = Local<Context>::New(isolate, context);
    Context::Scope context_scope(local_context);

    v8::Local<v8::Private> privateKey = v8::Private::ForApi(isolate, string_wrap.Get(isolate));
    auto local = v8::Local<v8::Object>::New(isolate, data->object);

    if (local.IsEmpty()) {
      return;
    }

    v8::Local<v8::Context> context = isolate->GetCurrentContext();

    local->DeletePrivate(context, privateKey);
}

V8Context::~V8Context() {
    for (ObjectDataMap::iterator it = seen_perl.begin(); it != seen_perl.end(); it++) {
        it->second->context = NULL;
    }
    seen_perl.clear();

    for (ObjectMap::iterator it = prototypes.begin(); it != prototypes.end(); it++) {
      it->second.Reset();
    }
    context.ClearWeak();
    string_wrap.Reset();
    make_function.Reset();
}

void
V8Context::bind(const char *name, SV *thing) {
    Isolate::Scope isolate_scope(isolate);
    HandleScope scope(isolate);

    Local<Context> local_context = Local<Context>::New(isolate, context);
    Context::Scope context_scope(local_context);

    local_context->Global()->Set(v8::String::NewFromUtf8(isolate, name, v8::String::kNormalString), sv2v8(thing));
}

void
V8Context::bind_ro(const char *name, SV *thing) {
    Isolate::Scope isolate_scope(isolate);
    HandleScope scope(isolate);

    Local<Context> local_context = Local<Context>::New(isolate, context);
    Context::Scope context_scope(local_context);

    bool result = context.Get(isolate)->Global()->DefineOwnProperty(
        isolate->GetCurrentContext(),
        v8::String::NewFromUtf8(isolate, name, v8::String::kNormalString),
        sv2v8(thing),
        v8::PropertyAttribute(v8::ReadOnly | v8::DontDelete)
    ).IsJust();
}

void V8Context::name_global(const char *name) {
    HandleScope scope(isolate);

    Local<Context> local_context = Local<Context>::New(isolate, context);
    Context::Scope context_scope(local_context);

    bool result = context.Get(isolate)->Global()->DefineOwnProperty(
        isolate->GetCurrentContext(),
        v8::String::NewFromUtf8(isolate, name, v8::String::kNormalString),
        context.Get(isolate)->Global(),
        v8::PropertyAttribute(v8::ReadOnly | v8::DontDelete)
    ).IsJust();
}

// I fucking hate pthreads, this lacks error handling, but hopefully works.
class thread_canceller {
public:
    thread_canceller(int sec)
        : sec_(sec)
    {
        if (sec_) {
            pthread_cond_init(&cond_, NULL);
            pthread_mutex_init(&mutex_, NULL);
            pthread_mutex_lock(&mutex_); // passed locked to canceller
            pthread_create(&id_, NULL, canceller, this);
        }
    }

    ~thread_canceller() {
        if (sec_) {
            pthread_mutex_lock(&mutex_);
            pthread_cond_signal(&cond_);
            pthread_mutex_unlock(&mutex_);
            void *ret;
            pthread_join(id_, &ret);
            pthread_mutex_destroy(&mutex_);
            pthread_cond_destroy(&cond_);
        }
    }

private:

    static void* canceller(void* this_) {
        thread_canceller* me = static_cast<thread_canceller*>(this_);
        struct timeval tv;
        struct timespec ts;
        gettimeofday(&tv, NULL);
        ts.tv_sec = tv.tv_sec + me->sec_;
        ts.tv_nsec = tv.tv_usec * 1000;

        if (pthread_cond_timedwait(&me->cond_, &me->mutex_, &ts) == ETIMEDOUT) {
            isolate->TerminateExecution();
        }
        pthread_mutex_unlock(&me->mutex_);
        return NULL;
    }

    pthread_t id_;
    pthread_cond_t cond_;
    pthread_mutex_t mutex_;
    int sec_;
};

SV*
V8Context::eval(SV* source, SV* origin) {
    Isolate::Scope isolate_scope(isolate);
    HandleScope handle_scope(isolate);
    TryCatch try_catch(isolate);
    Local<Context> local_context = context.Get(isolate);
    Context::Scope context_scope(local_context);

    // V8 expects everything in UTF-8, ensure SVs are upgraded.
    sv_utf8_upgrade(source);
    ScriptOrigin orig(origin ? sv2v8str(origin) : String::NewFromUtf8(isolate, "eval", v8::String::kNormalString));
    MaybeLocal<Script> script = Script::Compile(
        local_context,
        sv2v8str(source),
        &orig
    );

    if (try_catch.HasCaught()) {
        set_perl_error(local_context, try_catch);
        return &PL_sv_undef;
    }
    thread_canceller canceller(time_limit_);
    MaybeLocal<Value> val = script.ToLocalChecked()->Run(local_context);

    if (val.IsEmpty()) {
        set_perl_error(local_context, try_catch);
        return &PL_sv_undef;
    }
    sv_setsv(ERRSV,&PL_sv_undef);
    if (GIMME_V == G_VOID) {
        return &PL_sv_undef;
    }
    return v82sv(val.ToLocalChecked());
}

Handle<Value>
V8Context::sv2v8(SV *sv, HandleMap& seen) {
    if (SvROK(sv))
        return rv2v8(sv, seen);
    if (SvPOK(sv)) {
        // Upgrade string to UTF-8 if needed
        char *utf8 = SvPVutf8_nolen(sv);
        return String::NewFromUtf8(isolate, utf8, v8::String::kNormalString);
    }
    if (SvUOK(sv)) {
        UV v = SvUV(sv);
        return (v < 0xffffffffUL) ? (Handle<Number>)Integer::NewFromUnsigned(isolate, v) : Number::New(isolate, SvNV(sv));
    }
    if (SvIOK(sv)) {
        IV v = SvIV(sv);
        return (v <= INT32_MAX && v >= INT32_MIN) ? (Handle<Number>)Integer::New(isolate, v) : Number::New(isolate, SvNV(sv));
    }
    if (SvNOK(sv))
        return Number::New(isolate, SvNV(sv));
    if (!SvOK(sv))
        return Undefined(isolate);

    warn("Unknown sv type in sv2v8");
    return Undefined(isolate);
}

Handle<Value>
V8Context::sv2v8(SV *sv) {
    HandleMap seen;
    return sv2v8(sv, seen);
}

Handle<String> V8Context::sv2v8str(SV* sv)
{
    // Upgrade string to UTF-8 if needed
    char *utf8 = SvPVutf8_nolen(sv);
    return String::NewFromUtf8(isolate, utf8, v8::String::kNormalString, SvCUR(sv));
}

SV* V8Context::seen_v8(Handle<Object> object) {
    v8::Local<v8::Private> privateKey = v8::Private::ForApi(isolate, string_wrap.Get(isolate));
    v8::Local<v8::Value> value;

    v8::Local<v8::Context> context = isolate->GetCurrentContext();

    if (!object->HasPrivate(context, privateKey).ToChecked()) {
        return NULL;
    }

    if (object->GetPrivate(context, privateKey).ToLocal(&value)) {
        ObjectData* data = (ObjectData*) value.As<v8::External>()->Value();

        return newRV(data->sv);
    }

    return NULL;
}

SV *
V8Context::v82sv(Handle<Value> value, SvMap& seen) {
    if (value->IsUndefined())
        return newSV(0);

    if (value->IsNull())
        return newSV(0);

    Local<Context> ctx = this->get_local_context();
    if (value->IsInt32())
        return newSViv(value->Int32Value(ctx).FromMaybe(0));

    if (value->IsBoolean())
        return newSVuv(value->Uint32Value(ctx).FromMaybe(0));

    if (value->IsNumber())
        return newSVnv(value->NumberValue(ctx).FromMaybe(0));

    if (value->IsString()) {
        String::Utf8Value str(isolate, value);
        SV *sv = newSVpvn(*str, str.length());
        sv_utf8_decode(sv);
        return sv;
    }

    if (value->IsArray() || value->IsObject() || value->IsFunction()) {
        Handle<Object> object = value->ToObject(isolate);

        if (SV *cached = seen_v8(object))
            return cached;

        if (value->IsFunction()) {
            Handle<Function> fn = Handle<Function>::Cast(value);
            return function2sv(fn);
        }

        if (SV* cached = seen.find(ctx, object))
            return cached;

        if (value->IsArray()) {
            Handle<Array> array = Handle<Array>::Cast(value);
            return array2sv(array, seen);
        }

        if (value->IsObject()) {
            Handle<Object> object = Handle<Object>::Cast(value);
            return object2sv(object, seen);
        }
    }

    warn("Unknown v8 value in v82sv");
    return &PL_sv_undef;
}

SV *
V8Context::v82sv(Handle<Value> value) {
    SvMap seen;
    return v82sv(value, seen);
}

void
V8Context::fill_prototype(Handle<Object> prototype, HV* stash) {
    HE *he;
    while ((he = hv_iternext(stash))) {
        SV *key = HeSVKEY_force(he);
        Local<String> name = v8::String::NewFromUtf8(isolate, SvPV_nolen(key), v8::String::kNormalString);

        if (prototype->Has(this->get_local_context(), name).FromMaybe(0))
            continue;

        prototype->Set(name, (new PerlMethodData(this, SvPV_nolen(key)))->object.Get(isolate));
    }
}

#if PERL_VERSION > 8
Handle<Object>
V8Context::get_prototype(SV *sv) {
    HV *stash = SvSTASH(sv);
    char *package = HvNAME(stash);

    std::string pkg(package);
    ObjectMap::iterator it;

    Persistent<Object, CopyablePersistentTraits<Object>> prototype;

    it = prototypes.find(pkg);
    if (it != prototypes.end()) {
        prototype = it->second;
    }
    else {
        Local<Object> object = Object::New(isolate);
        Persistent<Object, CopyablePersistentTraits<Object>> persistent_object(isolate, object);
        prototype = prototypes[pkg] = persistent_object;
        //prototype = prototypes[pkg] = Persistent<Object>::New(isolate, Object::New(isolate));

        if (AV *isa = mro_get_linear_isa(stash)) {
            for (int i = 0; i <= av_len(isa); i++) {
                SV **sv = av_fetch(isa, i, 0);
                HV *stash = gv_stashsv(*sv, 0);
                fill_prototype(prototype.Get(isolate), stash);
            }
        }
    }

    return prototype.Get(isolate);
}
#endif

Handle<Value>
V8Context::rv2v8(SV *rv, HandleMap& seen) {
    SV* sv = SvRV(rv);
    long ptr = PTR2IV(sv);

    {
        ObjectDataMap::iterator it = seen_perl.find(ptr);
        if (it != seen_perl.end())
            return it->second->object.Get(isolate);
    }

    {
        HandleMap::const_iterator it = seen.find(ptr);
        if (it != seen.end())
            return it->second;
    }

#if PERL_VERSION > 8
    if (SvOBJECT(sv)) {
        const char *Perl_class = sv_reftype(sv, 1);
        if ((0 == strcmp(Perl_class, "JSON::PP::Boolean"))
            || (0 == strcmp(Perl_class, "JSON::XS::Boolean"))
        )
            return Boolean::New(isolate, sv_2bool(sv));
        return blessed2object(sv);
    }
#endif

    unsigned t = SvTYPE(sv);

    if (t == SVt_PVAV)
        return av2array((AV*)sv, seen, ptr);

    if (t == SVt_PVHV)
        return hv2object((HV*)sv, seen, ptr);

    if (t == SVt_PVCV)
        return cv2function((CV*)sv);

    warn("Unknown reference type in sv2v8()");
    return Undefined(isolate);
}

#if PERL_VERSION > 8
Handle<Object>
V8Context::blessed2object(SV *sv) {
    Handle<Object> object = Object::New(isolate);
    (void)object->SetPrototype(this->get_local_context(), get_prototype(sv)).FromMaybe(0);

    return (new PerlObjectData(this, object, sv))->object.Get(isolate);
}
#endif

Handle<Array>
V8Context::av2array(AV *av, HandleMap& seen, long ptr) {
    I32 i, len = av_len(av) + 1;
    Handle<Array> array = Array::New(isolate, len);
    seen[ptr] = array;
    for (i = 0; i < len; i++) {
        if (SV** sv = av_fetch(av, i, 0)) {
            array->Set(Integer::New(isolate, i), sv2v8(*sv, seen));
        }
    }
    return array;
}

Handle<Object>
V8Context::hv2object(HV *hv, HandleMap& seen, long ptr) {
    I32 len;
    char *key;
    SV *val;

    hv_iterinit(hv);
    Handle<Object> object = Object::New(isolate);
    seen[ptr] = object;
    while (val = hv_iternextsv(hv, &key, &len)) {
        object->Set(v8::String::NewFromUtf8(isolate, key, v8::String::kNormalString), sv2v8(val, seen));
    }
    return object;
}

Handle<Object>
V8Context::cv2function(CV *cv) {
    return (new PerlFunctionData(this, (SV*)cv))->object.Get(isolate);
}

SV*
V8Context::array2sv(Handle<Array> array, SvMap& seen) {
    AV *av = newAV();
    SV *rv = newRV_noinc((SV*)av);

    seen.add(array, PTR2IV(av));

    for (int i = 0; i < array->Length(); i++) {
        Handle<Value> elementVal = array->Get( Integer::New( isolate, i ) );
        av_push(av, v82sv(elementVal, seen));
    }
    return rv;
}

SV *
V8Context::object2sv(Handle<Object> obj, SvMap& seen) {
    if (enable_blessing && obj->Has(this->get_local_context(), v8::String::NewFromUtf8(isolate, "__perlPackage", v8::String::kNormalString)).FromMaybe(0)) {
        return object2blessed(obj);
    }

    HV *hv = newHV();
    SV *rv = newRV_noinc((SV*)hv);

    seen.add(obj, PTR2IV(hv));

    Local<Array> properties = obj->GetPropertyNames(this->get_local_context()).ToLocalChecked();
    for (int i = 0; i < properties->Length(); i++) {
        Local<Integer> propertyIndex = Integer::New( isolate, i );
        Local<String> propertyName = Local<String>::Cast( properties->Get( propertyIndex ) );
        String::Utf8Value propertyNameUTF8( isolate, propertyName );

        Local<Value> propertyValue = obj->Get( propertyName );
        if (*propertyValue)
            hv_store(hv, *propertyNameUTF8, 0 - propertyNameUTF8.length(), v82sv(propertyValue, seen), 0);
    }
    return rv;
}

static void
my_gv_setsv(pTHX_ GV* const gv, SV* const sv){
    ENTER;
    SAVETMPS;

    sv_setsv_mg((SV*)gv, sv_2mortal(newRV_inc((sv))));

    FREETMPS;
    LEAVE;
}

#ifdef dVAR
    #define DVAR dVAR;
#endif

#define SETUP_V8_CALL(ARGS_OFFSET) \
    DVAR \
    dXSARGS; \
\
    bool die = false; \
    int count = 1; \
\
    { \
        /* We have to do all this inside a block so that all the proper \
         * destructors are called if we need to croak. If we just croak in the \
         * middle of the block, v8 will segfault at program exit. */ \
        Isolate::Scope  isolate_scope(isolate); \
        HandleScope     scope(isolate); \
        TryCatch try_catch(isolate); \
        V8FunctionData* data = (V8FunctionData*)sv_object_data((SV*)cv); \
        if (data->context) { \
        V8Context      *self = data->context; \
        Handle<Context> ctx  = self->context.Get(isolate); \
        Context::Scope  context_scope(ctx); \
        vector<Handle<Value> > argv; \
\
        for (I32 i = ARGS_OFFSET; i < items; i++) { \
            argv.push_back(self->sv2v8(ST(i))); \
        }

#define CONVERT_V8_RESULT() \
        if (try_catch.HasCaught()) { \
            set_perl_error(ctx, try_catch); \
            die = true; \
        } \
        else { \
            if (data->returns_list && GIMME_V == G_ARRAY && result.ToLocalChecked()->IsArray()) { \
                Handle<Array> array = Handle<Array>::Cast(result.ToLocalChecked()); \
                if (GIMME_V == G_ARRAY) { \
                    count = array->Length(); \
                    EXTEND(SP, count - items); \
                    for (int i = 0; i < count; i++) { \
                        ST(i) = sv_2mortal(self->v82sv(array->Get(Integer::New(isolate, i)))); \
                    } \
                } \
                else { \
                    ST(0) = sv_2mortal(newSViv(array->Length())); \
                } \
            } \
            else { \
                ST(0) = sv_2mortal(self->v82sv(result.ToLocalChecked())); \
            } \
        } \
        } \
        else {\
            die = true; \
            sv_setpv(ERRSV, "Fatal error: V8 context is no more"); \
            sv_utf8_upgrade(ERRSV); \
        } \
    } \
\
    if (die) \
        croak(NULL); \
\
XSRETURN(count);

XS(v8closure) {
    SETUP_V8_CALL(0)
    MaybeLocal<Value> result = Handle<Function>::Cast(data->object.Get(isolate))->Call(ctx, ctx->Global(), items, &argv[0]);
    CONVERT_V8_RESULT()
}

XS(v8method) {
    SETUP_V8_CALL(1)
    V8ObjectData* This = (V8ObjectData*)SvIV((SV*)SvRV(ST(0)));
    MaybeLocal<Value> result = Handle<Function>::Cast(data->object.Get(isolate))->Call(ctx, This->object.Get(isolate), items - 1, &argv[0]);
    CONVERT_V8_RESULT();
}

SV*
V8Context::function2sv(Handle<Function> fn) {
    CV          *code = newXS(NULL, v8closure, __FILE__);
    V8ObjectData *data = new V8FunctionData(this, fn->ToObject(isolate), (SV*)code);
    return newRV_noinc((SV*)code);
}

SV*
V8Context::object2blessed(Handle<Object> obj) {
    char package[128];

    String::Utf8Value stringified( isolate, obj->Get( String::NewFromUtf8(isolate, "__perlPackage") )->ToString(isolate));

    snprintf(
        package,
        128,
        "%s%s::N%d",
        bless_prefix.c_str(),
        *stringified,
        number
    );

    HV *stash = gv_stashpv(package, 0);

    if (!stash) {
        Local<Object> prototype = obj->GetPrototype()->ToObject(isolate);

        stash = gv_stashpv(package, GV_ADD);

        Local<Array> properties = prototype->GetPropertyNames(this->get_local_context()).ToLocalChecked();
        for (int i = 0; i < properties->Length(); i++) {
            Local<String> name = properties->Get(i)->ToString(isolate);
            Local<Value> property = prototype->Get(name);

            if (!property->IsFunction())
                continue;

            Local<Function> fn = Local<Function>::Cast(property);

            CV *code = newXS(NULL, v8method, __FILE__);
            V8ObjectData *data = new V8FunctionData(this, fn, (SV*)code);

            GV* gv = (GV*)*hv_fetch(stash, *String::Utf8Value(isolate, name), name->Length(), TRUE);
            gv_init(gv, stash, *String::Utf8Value(isolate, name), name->Length(), GV_ADDMULTI); /* vivify */
            my_gv_setsv(aTHX_ gv, (SV*)code);
        }
    }

    SV* rv = newSV(0);
    SV* sv = newSVrv(rv, package);
    V8ObjectData *data = new V8ObjectData(this, obj, sv);
    sv_setiv(sv, PTR2IV(data));

    return rv;
}

bool
V8Context::idle_notification() {
    /*
    HeapStatistics hs;
    V8::GetHeapStatistics(&hs);
    L(
        "%d %d %d\n",
        hs.total_heap_size(),
        hs.total_heap_size_executable(),
        hs.used_heap_size()
    );
    */

    isolate->LowMemoryNotification(); // force garbage collection

    return true;
}

int
V8Context::adjust_amount_of_external_allocated_memory(int change_in_bytes) {
    //return V8::AdjustAmountOfExternalAllocatedMemory(change_in_bytes);
}

void
V8Context::set_flags_from_string(char *str) {
    V8::SetFlagsFromString(str, strlen(str));
}