#include "hypervisor.h"

#include "domain.h"

static void dummyErrorFunc(void* userData, virErrorPtr err) {}

static Napi::Value dummyCallback(const Napi::CallbackInfo& info)
{
    Napi::Env env = info.Env();
    Napi::HandleScope scope(env);
    return env.Undefined();
}

Napi::FunctionReference Hypervisor::constructor;
Napi::Object Hypervisor::Init(Napi::Env env, Napi::Object exports)
{
    Napi::HandleScope scope(env);

    Napi::Function func = DefineClass(env, "Hypervisor", {

        InstanceMethod("connectOpen", &Hypervisor::ConnectOpen),
        InstanceMethod("connectClose", &Hypervisor::ConnectClose),
        InstanceMethod("connectListDomains", &Hypervisor::ConnectListDomains),
        InstanceMethod("connectListDefinedDomains",
            &Hypervisor::ConnectListDefinedDomains),

        InstanceMethod("domainCreateXML", &Hypervisor::DomainCreateXML),
        InstanceMethod("domainDefineXML", &Hypervisor::DomainDefineXML),
        InstanceMethod("domainGetInfo", &Hypervisor::DomainGetInfo),
        InstanceMethod("domainGetName", &Hypervisor::DomainGetName),
        InstanceMethod("domainLookupByID", &Hypervisor::DomainLookupByID)

    });

    constructor = Napi::Persistent(func);
    constructor.SuppressDestruct();

    exports.Set("Hypervisor", func);
    return exports;
}

/******************************************************************************
 * Constructor                                                                *
 ******************************************************************************/

Hypervisor::Hypervisor(const Napi::CallbackInfo& info)
    : Napi::ObjectWrap<Hypervisor>(info)
{
    Napi::Env env = info.Env();
    Napi::HandleScope scope(env);

    if (info.Length() <= 0 || !info[0].IsObject())
    {
        Napi::TypeError::New(env, "Expected an object.")
            .ThrowAsJavaScriptException();
        return;
    }

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

    if (!options.HasOwnProperty("uri"))
    {
        Napi::TypeError::New(env, "Expected options to have property 'uri'.")
            .ThrowAsJavaScriptException();
        return;
    }

    Napi::String uri = options.Get("uri").As<Napi::String>();
    this->uri = std::string(uri.Utf8Value());
}

/******************************************************************************
 * ConnectOpen                                                                *
 ******************************************************************************/

class ConnectOpenWorker : public Napi::AsyncWorker {
public:

    ConnectOpenWorker(Napi::Function& callback,
        Napi::Promise::Deferred deferred, Hypervisor* hypervisor)
        : Napi::AsyncWorker(callback), deferred(deferred),
          hypervisor(hypervisor) {}

    void Execute()
    {
        virSetErrorFunc(NULL, dummyErrorFunc);
        hypervisor->conn = virConnectOpen(hypervisor->uri.c_str());
        if (hypervisor->conn == NULL)
        {
            virErrorPtr err = virGetLastError();
            SetError(std::string(err->message));
        }
    }

    void OnError(const Napi::Error& err)
    {
        Napi::HandleScope scope(Env());
        deferred.Reject(Napi::String::New(Env(), err.Message()));
        Callback().Call({});
    }

    void OnOK()
    {
        Napi::HandleScope scope(Env());
        deferred.Resolve(Env().Undefined());
        Callback().Call({});
    }

private:

    Napi::Promise::Deferred deferred;
    Hypervisor* hypervisor;

};

Napi::Value Hypervisor::ConnectOpen(const Napi::CallbackInfo& info)
{
    Napi::Env env = info.Env();
    Napi::HandleScope scope(env);

    Napi::Function callback = Napi::Function::New(env, dummyCallback);
    Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env);

    ConnectOpenWorker* worker = new ConnectOpenWorker(callback, deferred, this);
    worker->Queue();

    return deferred.Promise();
}

/******************************************************************************
 * ConnectClose                                                               *
 ******************************************************************************/

class ConnectCloseWorker : public Napi::AsyncWorker {
public:

    ConnectCloseWorker(Napi::Function& callback,
        Napi::Promise::Deferred deferred, Hypervisor* hypervisor)
        : Napi::AsyncWorker(callback), deferred(deferred),
          hypervisor(hypervisor) {}

    void Execute()
    {
        virConnectClose(hypervisor->conn);
    }

    void OnError(const Napi::Error& err)
    {
        Napi::HandleScope scope(Env());
        deferred.Reject(Napi::String::New(Env(), err.Message()));
        Callback().Call({});
    }

    void OnOK()
    {
        Napi::HandleScope scope(Env());
        deferred.Resolve(Env().Undefined());
        Callback().Call({});
    }

private:

    Napi::Promise::Deferred deferred;
    Hypervisor* hypervisor;

};

Napi::Value Hypervisor::ConnectClose(const Napi::CallbackInfo& info)
{
    Napi::Env env = info.Env();
    Napi::HandleScope scope(env);

    Napi::Function callback = Napi::Function::New(env, dummyCallback);
    Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env);

    ConnectCloseWorker* worker =
        new ConnectCloseWorker(callback, deferred, this);
    worker->Queue();

    return deferred.Promise();
}

/******************************************************************************
 * ConnectListDomains                                                         *
 ******************************************************************************/

class ConnectListDomainsWorker : public Napi::AsyncWorker {
public:

    ConnectListDomainsWorker(Napi::Function& callback,
        Napi::Promise::Deferred deferred, Hypervisor* hypervisor)
        : Napi::AsyncWorker(callback), deferred(deferred),
          hypervisor(hypervisor) {}

    void Execute()
    {
        numActiveDomains = virConnectNumOfDomains(hypervisor->conn);
        activeDomainIds = (int*) malloc(sizeof(int) * numActiveDomains);
        numActiveDomains = virConnectListDomains(hypervisor->conn,
            activeDomainIds, numActiveDomains);
    }

    void OnError(const Napi::Error& err)
    {
        Napi::HandleScope scope(Env());
        deferred.Reject(Napi::String::New(Env(), err.Message()));
        Callback().Call({});
    }

    void OnOK()
    {
        Napi::HandleScope scope(Env());

        Napi::Array activeDomains = Napi::Array::New(Env()); 
        for (int i = 0; i < numActiveDomains; i++)
        {
            activeDomains.Set(i, activeDomainIds[i]);
        }
        free(activeDomainIds);

        deferred.Resolve(activeDomains);
        Callback().Call({});
    }

private:

    Napi::Promise::Deferred deferred;
    Hypervisor* hypervisor;
    int numActiveDomains;
    int* activeDomainIds;

};

Napi::Value Hypervisor::ConnectListDomains(const Napi::CallbackInfo& info)
{
    Napi::Env env = info.Env();
    Napi::HandleScope scope(env);

    Napi::Function callback = Napi::Function::New(env, dummyCallback);
    Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env);

    ConnectListDomainsWorker* worker =
        new ConnectListDomainsWorker(callback, deferred, this);
    worker->Queue();

    return deferred.Promise();
}

/******************************************************************************
 * ConnectListDefinedDomains                                                  *
 ******************************************************************************/

class ConnectListDefinedDomainsWorker : public Napi::AsyncWorker {
public:

    ConnectListDefinedDomainsWorker(Napi::Function& callback,
        Napi::Promise::Deferred deferred, Hypervisor* hypervisor)
        : Napi::AsyncWorker(callback), deferred(deferred),
          hypervisor(hypervisor) {}

    void Execute()
    {
        numInactiveDomains =
            virConnectNumOfDefinedDomains(hypervisor->conn);
        inactiveDomainNames = (char**) malloc(
            sizeof(char*) * numInactiveDomains);
        numInactiveDomains = virConnectListDefinedDomains(hypervisor->conn,
            inactiveDomainNames, numInactiveDomains);
    }

    void OnError(const Napi::Error& err)
    {
        Napi::HandleScope scope(Env());
        deferred.Reject(Napi::String::New(Env(), err.Message()));
        Callback().Call({});
    }

    void OnOK()
    {
        Napi::HandleScope scope(Env());

        Napi::Array inactiveDomains = Napi::Array::New(Env()); 
        for (int i = 0; i < numInactiveDomains; i++)
        {
            inactiveDomains.Set(i, inactiveDomainNames[i]);
            free(inactiveDomainNames[i]);
        }
        free(inactiveDomainNames);

        deferred.Resolve(inactiveDomains);
        Callback().Call({});
    }

private:

    Napi::Promise::Deferred deferred;
    Hypervisor* hypervisor;
    int numInactiveDomains;
    char** inactiveDomainNames;

};

Napi::Value Hypervisor::ConnectListDefinedDomains(
    const Napi::CallbackInfo& info)
{
    Napi::Env env = info.Env();
    Napi::HandleScope scope(env);

    Napi::Function callback = Napi::Function::New(env, dummyCallback);
    Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env);

    ConnectListDefinedDomainsWorker* worker =
        new ConnectListDefinedDomainsWorker(callback, deferred, this);
    worker->Queue();

    return deferred.Promise();
}

/******************************************************************************
 * DomainCreateXML                                                            *
 ******************************************************************************/

class DomainCreateXMLWorker : public Napi::AsyncWorker {
public:

    DomainCreateXMLWorker(Napi::Function& callback,
        Napi::Promise::Deferred deferred, Hypervisor* hypervisor,
        std::string domainXml)
        : Napi::AsyncWorker(callback), deferred(deferred),
          hypervisor(hypervisor), domainXml(domainXml) {}

    void Execute()
    {
        domainPtr = virDomainCreateXML(hypervisor->conn, domainXml.c_str(), 0);
        if (!domainPtr)
        {
            virErrorPtr err = virGetLastError();
            SetError(std::string(err->message));
        }
    }

    void OnError(const Napi::Error& err)
    {
        Napi::HandleScope scope(Env());
        deferred.Reject(Napi::String::New(Env(), err.Message()));
        Callback().Call({});
    }

    void OnOK()
    {
        Napi::HandleScope scope(Env());

        Napi::Object domain = Domain::constructor.New({
            Napi::External<virDomain>::New(Env(), domainPtr) });

        deferred.Resolve(domain);
        Callback().Call({});
    }

private:

    Napi::Promise::Deferred deferred;
    Hypervisor* hypervisor;
    std::string domainXml;
    virDomainPtr domainPtr;

};

Napi::Value Hypervisor::DomainCreateXML(const Napi::CallbackInfo& info)
{
    Napi::Env env = info.Env();
    Napi::HandleScope scope(env);

    Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env);
    Napi::Function callback = Napi::Function::New(env, dummyCallback);

    if (info.Length() <= 0 || !info[0].IsString())
    {
        deferred.Reject(Napi::String::New(env, "Expected a string"));
        return deferred.Promise();
    }

    Napi::String domainXml = info[0].As<Napi::String>();

    DomainCreateXMLWorker* worker =
        new DomainCreateXMLWorker(callback, deferred, this, domainXml);
    worker->Queue();

    return deferred.Promise();
}

/******************************************************************************
 * DomainDefineXML                                                            *
 ******************************************************************************/

class DomainDefineXMLWorker : public Napi::AsyncWorker {
public:

    DomainDefineXMLWorker(Napi::Function& callback,
        Napi::Promise::Deferred deferred, Hypervisor* hypervisor,
        std::string domainXml)
        : Napi::AsyncWorker(callback), deferred(deferred),
          hypervisor(hypervisor), domainXml(domainXml) {}

    void Execute()
    {
        domainPtr = virDomainDefineXML(hypervisor->conn, domainXml.c_str());
        if (!domainPtr)
        {
            virErrorPtr err = virGetLastError();
            SetError(std::string(err->message));
        }
    }

    void OnError(const Napi::Error& err)
    {
        Napi::HandleScope scope(Env());
        deferred.Reject(Napi::String::New(Env(), err.Message()));
        Callback().Call({});
    }

    void OnOK()
    {
        Napi::HandleScope scope(Env());

        Napi::Object domain = Domain::constructor.New({
            Napi::External<virDomain>::New(Env(), domainPtr) });

        deferred.Resolve(domain);
        Callback().Call({});
    }

private:

    Napi::Promise::Deferred deferred;
    Hypervisor* hypervisor;
    std::string domainXml;
    virDomainPtr domainPtr;

};

Napi::Value Hypervisor::DomainDefineXML(const Napi::CallbackInfo& info)
{
    Napi::Env env = info.Env();
    Napi::HandleScope scope(env);

    Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env);
    Napi::Function callback = Napi::Function::New(env, dummyCallback);

    if (info.Length() <= 0 || !info[0].IsString())
    {
        deferred.Reject(Napi::String::New(env, "Expected a string"));
        return deferred.Promise();
    }

    Napi::String domainXml = info[0].As<Napi::String>();

    DomainDefineXMLWorker* worker =
        new DomainDefineXMLWorker(callback, deferred, this, domainXml);
    worker->Queue();

    return deferred.Promise();
}

/******************************************************************************
 * DomainGetInfo                                                              *
 ******************************************************************************/

class DomainGetInfoWorker : public Napi::AsyncWorker {
public:

    DomainGetInfoWorker(Napi::Function& callback,
        Napi::Promise::Deferred deferred, Domain* domain)
        : Napi::AsyncWorker(callback), deferred(deferred), domain(domain) {}

    void Execute()
    {
        int ret = virDomainGetInfo(domain->domainPtr, &domainInfo);
        if (ret < 0)
        {
            virErrorPtr err = virGetLastError();
            SetError(std::string(err->message));
        }
    }

    void OnError(const Napi::Error& err)
    {
        Napi::HandleScope scope(Env());
        deferred.Reject(Napi::String::New(Env(), err.Message()));
        Callback().Call({});
    }

    void OnOK()
    {
        Napi::HandleScope scope(Env());

        Napi::Object info = Napi::Object::New(Env());
        info.Set("state", Napi::Number::New(Env(), domainInfo.state));
        info.Set("maxMem", Napi::Number::New(Env(), domainInfo.maxMem));
        info.Set("memory", Napi::Number::New(Env(), domainInfo.memory));
        info.Set("nrVirtCpu", Napi::Number::New(Env(), domainInfo.nrVirtCpu));
        info.Set("cpuTime", Napi::Number::New(Env(), domainInfo.cpuTime));

        deferred.Resolve(info);
        Callback().Call({});
    }

private:

    Napi::Promise::Deferred deferred;
    Domain* domain;
    virDomainInfo domainInfo;

};

Napi::Value Hypervisor::DomainGetInfo(const Napi::CallbackInfo& info)
{
    Napi::Env env = info.Env();
    Napi::HandleScope scope(env);

    Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env);
    Napi::Function callback = Napi::Function::New(env, dummyCallback);

    if (info.Length() <= 0 || !info[0].IsObject())
    {
        deferred.Reject(Napi::String::New(env, "Expected an object."));
        return deferred.Promise();
    }

    Domain* domain = Napi::ObjectWrap<Domain>::Unwrap(
        info[0].As<Napi::Object>());

    DomainGetInfoWorker* worker = new DomainGetInfoWorker(
        callback, deferred, domain);
    worker->Queue();

    return deferred.Promise();
}

/******************************************************************************
 * DomainGetName                                                              *
 ******************************************************************************/

class DomainGetNameWorker : public Napi::AsyncWorker {
public:

    DomainGetNameWorker(Napi::Function& callback,
        Napi::Promise::Deferred deferred, Domain* domain)
        : Napi::AsyncWorker(callback), deferred(deferred), domain(domain) {}

    void Execute()
    {
        domainName = virDomainGetName(domain->domainPtr);
        if (domainName == NULL)
        {
            virErrorPtr err = virGetLastError();
            SetError(std::string(err->message));
        }
    }

    void OnError(const Napi::Error& err)
    {
        Napi::HandleScope scope(Env());
        deferred.Reject(Napi::String::New(Env(), err.Message()));
        Callback().Call({});
    }

    void OnOK()
    {
        Napi::HandleScope scope(Env());
        deferred.Resolve(Napi::String::New(Env(), this->domainName));
        Callback().Call({});
    }

private:

    Napi::Promise::Deferred deferred;
    Domain* domain;
    const char* domainName;

};

Napi::Value Hypervisor::DomainGetName(const Napi::CallbackInfo& info)
{
    Napi::Env env = info.Env();
    Napi::HandleScope scope(env);

    Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env);
    Napi::Function callback = Napi::Function::New(env, dummyCallback);

    if (info.Length() <= 0 || !info[0].IsObject())
    {
        deferred.Reject(Napi::String::New(env, "Expected an object."));
        return deferred.Promise();
    }

    Domain* domain = Napi::ObjectWrap<Domain>::Unwrap(
        info[0].As<Napi::Object>());

    DomainGetNameWorker* worker = new DomainGetNameWorker(
        callback, deferred, domain);
    worker->Queue();

    return deferred.Promise();
}

/******************************************************************************
 * DomainLookupByID                                                           *
 ******************************************************************************/

class DomainLookupByIDWorker : public Napi::AsyncWorker {
public:

    DomainLookupByIDWorker(Napi::Function& callback,
        Napi::Promise::Deferred deferred, Hypervisor* hypervisor, int id)
        : Napi::AsyncWorker(callback), deferred(deferred),
          hypervisor(hypervisor), id(id) {}

    void Execute()
    {
        domainPtr = virDomainLookupByID(hypervisor->conn, id);
        if (domainPtr == NULL)
        {
            virErrorPtr err = virGetLastError();
            SetError(std::string(err->message));
        }
    }

    void OnError(const Napi::Error& err)
    {
        Napi::HandleScope scope(Env());
        deferred.Reject(Napi::String::New(Env(), err.Message()));
        Callback().Call({});
    }

    void OnOK()
    {
        Napi::HandleScope scope(Env());

        Napi::Object domain = Domain::constructor.New({
            Napi::External<virDomain>::New(Env(), domainPtr) });

        deferred.Resolve(domain);
        Callback().Call({});
    }

private:

    Napi::Promise::Deferred deferred;
    Hypervisor *hypervisor;
    int id;
    virDomainPtr domainPtr;

};

Napi::Value Hypervisor::DomainLookupByID(const Napi::CallbackInfo& info)
{
    Napi::Env env = info.Env();
    Napi::HandleScope scope(env);

    Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env);
    Napi::Function callback = Napi::Function::New(env, dummyCallback);

    if (info.Length() <= 0 || !info[0].IsNumber())
    {
        deferred.Reject(Napi::String::New(env, "Expected a number."));
        return deferred.Promise();
    }

    int id = info[0].As<Napi::Number>();

    DomainLookupByIDWorker* worker = new DomainLookupByIDWorker(
        callback, deferred, this, id);
    worker->Queue();

    return deferred.Promise();
}
