/*
 * Copyright (C) 2012, Nexenta Systems, Inc.
 *
 * The contents of this file are subject to the terms of
 * the Common Development and Distribution License ("CDDL").
 * You may not use this file except in compliance with this license.
 *
 * You can obtain a copy of the License at
 * http://www.opensource.org/licenses/CDDL-1.0
 */

#include <string>

#define BUILDING_NODE_EXTENSION 1

// node.h includes v8.h
#include <node.h>
#include <nan.h>

extern "C" { // Yes, that bad
#include <augeas.h>
}

using namespace v8;
template<class T>
using Handle = v8::Local<T>;

inline v8::Isolate *isol() {
    return v8::Isolate::GetCurrent();
}

inline v8::Local<v8::Context> ctx() {
    return v8::Isolate::GetCurrent()->GetCurrentContext();
}

inline std::string aug_error_msg(augeas *aug) {
    std::string msg = aug_error_message(aug);
    const char *minor = aug_error_minor_message(aug);
    const char *details = aug_error_details(aug);
    if (NULL != minor) {
        msg += " - ";
        msg += minor;
    }
    if (NULL != details) {
        msg += ": ";
        msg += details;
    }
    return msg;
}

inline void throw_aug_error_msg(augeas *aug) {
    if (AUG_NOERROR != aug_error(aug)) {
        Nan::ThrowError(aug_error_msg(aug).c_str());
    } else {
        Nan::ThrowError("An error has occured from Augeas API call, but no "
                        "description available");
    }
}

/*
 * Helper function.
 * Converts value of object member *key into std::string.
 * Returns empty string if memder does not exist.
 */
inline std::string memberToString(Handle<Object> obj, const char *key) {
    Local<Value> m = obj->Get(ctx(), Nan::New<String>(key).ToLocalChecked()).ToLocalChecked();
    if (!m->IsUndefined()) {
        String::Utf8Value str(isol(), m);
        return std::string(*str);
    } else {
        return std::string();
    }
}

/*
 * Helper function.
 * Converts value of object member *key into uint32.
 * Returns 0 if memder does not exist.
 */
inline uint32_t memberToUint32(Handle<Object> obj, const char *key) {
    Local<Value> m = obj->Get(ctx(), Nan::New<String>(key).ToLocalChecked()).ToLocalChecked();
    if (!m->IsUndefined()) {
        return m->Uint32Value(ctx()).ToChecked();
    } else {
        return 0;
    }
}

/*
 * Helper function.
 * Joins JS array by new line.
 */
inline std::string join(Local<Array> a) {
    std::string res;
    uint32_t len = a->Length();
    if (len > 0) {
        for (uint32_t i = 0; i < len - 1; ++i) {
            String::Utf8Value v(isol(), a->Get(ctx(), i).ToLocalChecked());
            res.append(*v);
            res.append("\n");
        }
        String::Utf8Value v(isol(), a->Get(ctx(), len - 1).ToLocalChecked());
        res.append(*v);
    }
    return res;
}

class LibAugeas : public node::ObjectWrap {
  public:
    static void Init(Handle<Object> target);
    static Local<Object> New(augeas *aug);

  protected:
    augeas *m_aug;
    LibAugeas();
    ~LibAugeas();

    static Nan::Persistent<FunctionTemplate> augeasTemplate;
    static Nan::Persistent<Function> constructor;

    static NAN_METHOD(defvar);
    static NAN_METHOD(defnode);
    static NAN_METHOD(get);
    static NAN_METHOD(set);
    static NAN_METHOD(setm);
    static NAN_METHOD(rm);
    static NAN_METHOD(mv);
    static NAN_METHOD(save);
    static NAN_METHOD(nmatch);
    static NAN_METHOD(match);
    static NAN_METHOD(load);
    static NAN_METHOD(srun);
    static NAN_METHOD(insertAfter);
    static NAN_METHOD(insertBefore);
    static NAN_METHOD(error);
    static NAN_METHOD(errorMsg);
    static NAN_METHOD(errorLens);
    static NAN_METHOD(errorIncl);
    static NAN_METHOD(print);
};

Nan::Persistent<FunctionTemplate> LibAugeas::augeasTemplate;
Nan::Persistent<Function> LibAugeas::constructor;

void LibAugeas::Init(Handle<Object> target) {
    Nan::HandleScope scope;
    // flags for aug_init():
    NODE_DEFINE_CONSTANT(target, AUG_NONE);
    NODE_DEFINE_CONSTANT(target, AUG_SAVE_BACKUP);
    NODE_DEFINE_CONSTANT(target, AUG_SAVE_NEWFILE);
    NODE_DEFINE_CONSTANT(target, AUG_TYPE_CHECK);
    NODE_DEFINE_CONSTANT(target, AUG_NO_STDINC);
    NODE_DEFINE_CONSTANT(target, AUG_SAVE_NOOP);
    NODE_DEFINE_CONSTANT(target, AUG_NO_LOAD);
    NODE_DEFINE_CONSTANT(target, AUG_NO_MODL_AUTOLOAD);
    NODE_DEFINE_CONSTANT(target, AUG_ENABLE_SPAN);
    NODE_DEFINE_CONSTANT(target, AUG_NO_ERR_CLOSE);

    // error codes:
    NODE_DEFINE_CONSTANT(target, AUG_NOERROR);
    NODE_DEFINE_CONSTANT(target, AUG_ENOMEM);
    NODE_DEFINE_CONSTANT(target, AUG_EINTERNAL);
    NODE_DEFINE_CONSTANT(target, AUG_EPATHX);
    NODE_DEFINE_CONSTANT(target, AUG_ENOMATCH);
    NODE_DEFINE_CONSTANT(target, AUG_EMMATCH);
    NODE_DEFINE_CONSTANT(target, AUG_ESYNTAX);
    NODE_DEFINE_CONSTANT(target, AUG_ENOLENS);
    NODE_DEFINE_CONSTANT(target, AUG_EMXFM);
    NODE_DEFINE_CONSTANT(target, AUG_ENOSPAN);
    NODE_DEFINE_CONSTANT(target, AUG_EMVDESC);
    NODE_DEFINE_CONSTANT(target, AUG_ECMDRUN);
    NODE_DEFINE_CONSTANT(target, AUG_EBADARG);

    Local<FunctionTemplate> localTemplate = Nan::New<v8::FunctionTemplate>();
    augeasTemplate.Reset(localTemplate);
    localTemplate->SetClassName(Nan::New<String>("Augeas").ToLocalChecked());
    localTemplate->InstanceTemplate()->SetInternalFieldCount(1);

// I do not want copy-n-paste errors here:
#define _NEW_METHOD(m) Nan::SetPrototypeMethod(localTemplate, #m, m)
    _NEW_METHOD(defvar);
    _NEW_METHOD(defnode);
    _NEW_METHOD(get);
    _NEW_METHOD(set);
    _NEW_METHOD(setm);
    _NEW_METHOD(rm);
    _NEW_METHOD(mv);
    _NEW_METHOD(save);
    _NEW_METHOD(nmatch);
    _NEW_METHOD(match);
    _NEW_METHOD(load);
    _NEW_METHOD(srun);
    _NEW_METHOD(insertAfter);
    _NEW_METHOD(insertBefore);
    _NEW_METHOD(error);
    _NEW_METHOD(errorMsg);
    _NEW_METHOD(errorLens);
    _NEW_METHOD(errorIncl);
    _NEW_METHOD(print);

    constructor.Reset(localTemplate->GetFunction(ctx()).ToLocalChecked());
}

/*
 * Creates an JS object to pass it in callback function.
 * This JS objects wraps an LibAugeas object.
 * Only for using within this C++ code.
 */
Local<Object> LibAugeas::New(augeas *aug) {
    LibAugeas *obj = new LibAugeas();
    obj->m_aug = aug;
    Local<FunctionTemplate> localTemplate = Nan::New(augeasTemplate);
    Local<Object> O = localTemplate->InstanceTemplate()->NewInstance(ctx()).ToLocalChecked();
    obj->Wrap(O);
    return O;
}

/*
 * Wrapper of aug_defvar() - define a variable
 * The second argument is optional and if ommited,
 * variable will be removed if exists.
 *
 * On error, throws an exception and returns undefined;
 * on success, returns 0 if expr evaluates to anything
 * other than a nodeset, and the number of nodes if expr
 * evaluates to a nodeset
 */
NAN_METHOD(LibAugeas::defvar) {
    Nan::HandleScope scope;

    if (info.Length() < 1 || info.Length() > 2) {
        Nan::ThrowError("Wrong number of arguments");
        Nan::Undefined();
    }

    LibAugeas *obj = node::ObjectWrap::Unwrap<LibAugeas>(info.This());
    String::Utf8Value n_str(isol(), info[0]);
    String::Utf8Value e_str(isol(), info[1]);

    const char *name = *n_str;
    const char *expr = *e_str;

    /* Returns -1 on error; on success, returns 0 if EXPR evaluates to anything
     * other than a nodeset, and the number of nodes if EXPR evaluates to a
     * nodeset
     */
    int rc = aug_defvar(obj->m_aug, name, info[1]->IsUndefined() ? NULL : expr);
    if (-1 == rc) {
        throw_aug_error_msg(obj->m_aug);
        Nan::Undefined();
    } else {
        info.GetReturnValue().Set(Nan::New<Number>(rc));
    }
}

/*
 * Wrapper of aug_defnode() - define a node.
 * The defnode command is very useful when you add a node
 * that you need to modify further, e. g. by adding children to it.
 *
 * On error, throws an exception and returns undefined;
 * on success, returns the number of nodes in the nodeset (>= 0)
 *
 * Arguments:
 * name - required
 * expr - required
 * value - optional
 * callback - optional
 *
 * The last argument could be a function. It will be called (synchronously)
 * with one argument set to True if a node was created, and False if it already
 *existed.
 */
NAN_METHOD(LibAugeas::defnode) {
    Nan::HandleScope scope;

    if (info.Length() < 2 || info.Length() > 4) {
        Nan::ThrowError("Wrong number of arguments");
        Nan::Undefined();
    }

    LibAugeas *obj = node::ObjectWrap::Unwrap<LibAugeas>(info.This());
    String::Utf8Value n_str(isol(), info[0]);
    String::Utf8Value e_str(isol(), info[1]);
    String::Utf8Value v_str(isol(), info[2]);

    const char *name = *n_str;
    const char *expr = *e_str;
    const char *value = NULL;
    if (!info[2]->IsUndefined() && !info[2]->IsFunction()) {
        value = *v_str;
    }
    int created;

    /* Returns -1 on error; on success, returns
     * the number of nodes in the nodeset, set created=1 if node created,
     * set created=0 if node already existed.
     */
    int rc = aug_defnode(obj->m_aug, name, expr, value, &created);
    if (-1 == rc) {
        throw_aug_error_msg(obj->m_aug);
        Nan::Undefined();
    } else {
        int last = info.Length() - 1;
        if (info[last]->IsFunction()) {
            Nan::Callback cb(Local<Function>::Cast(info[last]));

            Local<Value> argv[] = { Nan::New<Boolean>(created == 1) };

            Nan::TryCatch try_catch;
            cb.Call(1, argv);
            if (try_catch.HasCaught()) {
                Nan::FatalException(try_catch);
            }
        }
        info.GetReturnValue().Set(Nan::New<Number>(rc));
    }
}

/*
 * Wrapper of aug_get() - get exactly one value
 */
NAN_METHOD(LibAugeas::get) {
    Nan::HandleScope scope;

    if (info.Length() != 1) {
        Nan::ThrowError("Function accepts exactly one argument");
        Nan::Undefined();
    }

    LibAugeas *obj = node::ObjectWrap::Unwrap<LibAugeas>(info.This());
    String::Utf8Value p_str(isol(), info[0]);

    const char *path = *p_str; // operator*() returns C-string
    const char *value;

    /*
     * Return 1 if there is exactly one node matching PATH,
     * 0 if there is none, and a negative value
     * if there is more than one node matching PATH,
     * or if PATH is not a legal path expression.
     *
     * The string *value must not be freed by the caller,
     * and is valid as long as its node remains unchanged.
     */
    int rc = aug_get(obj->m_aug, path, &value);
    if (1 == rc) {
        if (NULL != value) {
            info.GetReturnValue().Set(Nan::New<String>(value).ToLocalChecked());
        } else {
            Nan::Null();
        }
    } else if (0 == rc) {
        Nan::Undefined();
    } else if (rc < 0) {
        throw_aug_error_msg(obj->m_aug);
        Nan::Undefined();
    } else {
        Nan::ThrowError("Unexpected return value of aug_get()");
        Nan::Undefined();
    }
}

/*
 * Wrapper of aug_set() - set exactly one value
 * Note: this method (as aug_set) does not write any files,
 *       it just changes internal tree. To write files use LibAugeas::save()
 */
NAN_METHOD(LibAugeas::set) {
    Nan::HandleScope scope;

    if (info.Length() != 2) {
        Nan::ThrowError("Function accepts exactly two arguments");
        Nan::Undefined();
    }

    LibAugeas *obj = node::ObjectWrap::Unwrap<LibAugeas>(info.This());
    String::Utf8Value p_str(isol(), info[0]);
    String::Utf8Value v_str(isol(), info[1]);

    const char *path = *p_str;
    const char *value = *v_str;

    /*
     * 0 on success, -1 on error. It is an error
     * if more than one node matches path.
     */
    int rc = aug_set(obj->m_aug, path, value);
    if (AUG_NOERROR != rc) {
        throw_aug_error_msg(obj->m_aug);
    }
    Nan::Undefined();
}

/*
 * Wrapper of aug_setm() - set the value of multiple nodes in one operation
 * Returns the number of modified nodes on success.
 */
NAN_METHOD(LibAugeas::setm) {
    Nan::HandleScope scope;

    if (info.Length() != 3) {
        Nan::ThrowError("Function accepts exactly three arguments");
        Nan::Undefined();
    }

    LibAugeas *obj = node::ObjectWrap::Unwrap<LibAugeas>(info.This());
    String::Utf8Value b_str(isol(), info[0]);
    String::Utf8Value s_str(isol(), info[1]);
    String::Utf8Value v_str(isol(), info[2]);

    const char *base = *b_str;
    const char *sub = *s_str;
    const char *value = *v_str;

    int rc = aug_setm(obj->m_aug, base, sub, value);
    if (rc >= 0) {
        info.GetReturnValue().Set(Nan::New<Int32>(rc));
    } else {
        throw_aug_error_msg(obj->m_aug);
        Nan::Undefined();
    }
}

/*
 * Wrapper of aug_rm() - remove nodes
 * Remove path and all its children. Returns the number of entries removed.
 * All nodes that match PATH, and their descendants, are removed.
 */
NAN_METHOD(LibAugeas::rm) {
    Nan::HandleScope scope;

    if (info.Length() != 1) {
        Nan::ThrowError("Function accepts exactly one argument");
        Nan::Undefined();
    }

    LibAugeas *obj = node::ObjectWrap::Unwrap<LibAugeas>(info.This());
    String::Utf8Value p_str(isol(), info[0]);

    const char *path = *p_str;

    int rc = aug_rm(obj->m_aug, path);
    if (rc >= 0) {
        info.GetReturnValue().Set(Nan::New<Number>(rc));
    } else {
        throw_aug_error_msg(obj->m_aug);
        Nan::Undefined();
    }
}

/*
 * Wrapper of aug_mv() - move nodes
 */
NAN_METHOD(LibAugeas::mv) {
    Nan::HandleScope scope;

    if (info.Length() != 2) {
        Nan::ThrowError("Function accepts exactly two arguments");
        Nan::Undefined();
    }

    LibAugeas *obj = node::ObjectWrap::Unwrap<LibAugeas>(info.This());
    String::Utf8Value src(isol(), info[0]);
    String::Utf8Value dst(isol(), info[1]);

    const char *source = *src;
    const char *dest = *dst;

    /*
     * 0 on success, -1 on error. It is an error
     * if more than one node matches path.
     */
    int rc = aug_mv(obj->m_aug, source, dest);
    if (AUG_NOERROR != rc) {
        throw_aug_error_msg(obj->m_aug);
    }
    Nan::Undefined();
}

/*
 * Wrapper of aug_insert(aug, path, label, 0) - insert 'label' after 'path'
 */
NAN_METHOD(LibAugeas::insertAfter) {
    Nan::HandleScope scope;

    if (info.Length() != 2) {
        Nan::ThrowError("Function accepts exactly two arguments");
        Nan::Undefined();
    }

    LibAugeas *obj = node::ObjectWrap::Unwrap<LibAugeas>(info.This());
    String::Utf8Value p_str(isol(), info[0]);
    String::Utf8Value l_str(isol(), info[1]);

    const char *path = *p_str;
    const char *label = *l_str;

    int rc = aug_insert(obj->m_aug, path, label, 0);
    if (AUG_NOERROR != rc) {
        throw_aug_error_msg(obj->m_aug);
    }
    Nan::Undefined();
}

/*
 * Wrapper of aug_insert(aug, path, label, 1) - insert 'label' before 'path'
 */
NAN_METHOD(LibAugeas::insertBefore) {
    Nan::HandleScope scope;

    if (info.Length() != 2) {
        Nan::ThrowError("Function accepts exactly two arguments");
        Nan::Undefined();
    }

    LibAugeas *obj = node::ObjectWrap::Unwrap<LibAugeas>(info.This());
    String::Utf8Value p_str(isol(), info[0]);
    String::Utf8Value l_str(isol(), info[1]);

    const char *path = *p_str;
    const char *label = *l_str;

    int rc = aug_insert(obj->m_aug, path, label, 1);
    if (AUG_NOERROR != rc) {
        throw_aug_error_msg(obj->m_aug);
    }
    Nan::Undefined();
}

/*
 * Wrapper of aug_error()
 * Returns the error code from the last API call
 */
NAN_METHOD(LibAugeas::error) {
    Nan::HandleScope scope;

    if (info.Length() != 0) {
        Nan::ThrowError("Function does not accept arguments");
        Nan::Undefined();
    }

    LibAugeas *obj = node::ObjectWrap::Unwrap<LibAugeas>(info.This());

    int rc = aug_error(obj->m_aug);
    info.GetReturnValue().Set(Nan::New<Int32>(rc));
}

/*
 * Returns the error message from the last API call,
 * including all details.
 */
NAN_METHOD(LibAugeas::errorMsg) {
    Nan::HandleScope scope;

    if (info.Length() != 0) {
        Nan::ThrowError("Function does not accept arguments");
        Nan::Undefined();
    }

    LibAugeas *obj = node::ObjectWrap::Unwrap<LibAugeas>(info.This());

    info.GetReturnValue().Set(
        Nan::New<String>(aug_error_msg(obj->m_aug)).ToLocalChecked());
}

/*
 * Returns the lens load error message
 */
NAN_METHOD(LibAugeas::errorLens) {
    Nan::HandleScope scope;
    const char *val;

    if (info.Length() != 1) {
        Nan::ThrowError("Function expects lens argument");
        Nan::Undefined();
    }
    String::Utf8Value lens(isol(), info[0]);

    LibAugeas *obj = node::ObjectWrap::Unwrap<LibAugeas>(info.This());

    std::string errPath = "/augeas/load/" + std::string(*lens) + "/error";
    if (aug_get(obj->m_aug, errPath.c_str(), &val)) {
        info.GetReturnValue().Set(Nan::New<String>(val).ToLocalChecked());
    }

    Nan::Undefined();
}

/*
 * Returns the incl parsed error message
 */
NAN_METHOD(LibAugeas::errorIncl) {
    Nan::HandleScope scope;
    int mres;
    const char *val;
    char **matches;

    if (info.Length() != 1) {
        Nan::ThrowError("Function expects incl argument");
        Nan::Undefined();
    }
    String::Utf8Value incl(isol(), info[0]);

    LibAugeas *obj = node::ObjectWrap::Unwrap<LibAugeas>(info.This());

    std::string errPath = "/augeas/files" + std::string(*incl) + "/error";
    mres = aug_match(obj->m_aug, errPath.c_str(), &matches);
    if (mres) {
        Local<Object> res = Nan::New<Object>();

        if (aug_get(obj->m_aug, std::string(errPath + "/line").c_str(), &val)) {
            res->Set(ctx(), Nan::New<String>("line").ToLocalChecked(),
                     Nan::New<String>(val).ToLocalChecked());
        }
        if (aug_get(obj->m_aug, std::string(errPath + "/message").c_str(),
                    &val)) {
            res->Set(ctx(), Nan::New<String>("message").ToLocalChecked(),
                     Nan::New<String>(val).ToLocalChecked());
        }
        free(matches);
        info.GetReturnValue().Set(res);
    }

    Nan::Undefined();
}

struct SaveUV {
    uv_work_t request;
    Nan::Callback callback;
    augeas *aug;
    int rc; // = aug_save(), 0 on success, -1 on error
};

void saveWork(uv_work_t *req) {
    SaveUV *suv = static_cast<SaveUV *>(req->data);
    suv->rc = aug_save(suv->aug);
}

/*
 * Execute JS callback after saveWork() terminated
 */
void saveAfter(uv_work_t *req) {
    Nan::HandleScope scope;

    SaveUV *suv = static_cast<SaveUV *>(req->data);
    Local<Value> argv[] = { Nan::New<Int32>(suv->rc) };

    Nan::TryCatch try_catch;
    suv->callback.Call(1, argv);
    delete suv;
    if (try_catch.HasCaught()) {
        Nan::FatalException(try_catch);
    }
}

/*
 * Wrapper of aug_save() - save changed files.
 *
 * Without arguments this function performs blocking saving
 * and throws an exception on error.
 *
 * The only argument allowed is a callback function.
 * If such an argument is given this function performs
 * non-blocking (async) saving, and after saving is done (or failed)
 * executes the callback with one integer argument - return value of aug_save(),
 * i. e. 0 on success, -1 on failure.
 *
 * NOTE: multiple async calls of this function (from the same augeas object)
 * will result in crash (double free or segfault) because of using
 * shared augeas handle.
 *
 * Always returns undefined.
 */
NAN_METHOD(LibAugeas::save) {
    Nan::HandleScope scope;

    LibAugeas *obj = node::ObjectWrap::Unwrap<LibAugeas>(info.This());

    // if no info, save files synchronously (blocking):
    if (info.Length() == 0) {
        int rc = aug_save(obj->m_aug);
        if (AUG_NOERROR != rc) {
            Nan::ThrowError("Failed to write files");
        }
        // single argument is a function - async:
    } else if ((info.Length() == 1) && info[0]->IsFunction()) {
        SaveUV *suv = new SaveUV();
        suv->request.data = suv;
        suv->aug = obj->m_aug;
        suv->callback.SetFunction(Local<Function>::Cast(info[0]));
        uv_queue_work(uv_default_loop(), &suv->request, saveWork,
                      (uv_after_work_cb)saveAfter);
    } else {
        Nan::ThrowError("Callback function or nothing");
    }

    Nan::Undefined();
}

/*
 * Wrapper of aug_match(aug, path, NULL) - count all nodes matching path
 * expression
 * Returns the number of found nodes.
 * Note: aug_match() allocates memory if the third argument is not NULL,
 * in this function we always set it to NULL and get only number of found nodes.
 */
NAN_METHOD(LibAugeas::nmatch) {
    Nan::HandleScope scope;

    if (info.Length() != 1) {
        Nan::ThrowError("Function accepts exactly one argument");
        Nan::Undefined();
    }

    LibAugeas *obj = node::ObjectWrap::Unwrap<LibAugeas>(info.This());
    String::Utf8Value p_str(isol(), info[0]);

    const char *path = *p_str;

    int rc = aug_match(obj->m_aug, path, NULL);
    if (rc >= 0) {
        info.GetReturnValue().Set(Nan::New<Number>(rc));
    } else {
        throw_aug_error_msg(obj->m_aug);
        Nan::Undefined();
    }
}

/*
 * Wrapper of aug_match(, , non-NULL).
 * Returns an array of nodes matching given path expression
 */
NAN_METHOD(LibAugeas::match) {
    Nan::HandleScope scope;

    if (info.Length() != 1) {
        Nan::ThrowError("Function accepts exactly one argument");
        Nan::Undefined();
    }

    LibAugeas *obj = node::ObjectWrap::Unwrap<LibAugeas>(info.This());
    String::Utf8Value p_str(isol(), info[0]);

    const char *path = *p_str;
    char **matches = NULL;

    int rc = aug_match(obj->m_aug, path, &matches);
    if (rc >= 0) {
        Local<Array> result = Nan::New<Array>(rc);
        if (NULL != matches) {
            for (int i = 0; i < rc; ++i) {
                result->Set(ctx(), Nan::New<Number>(i),
                            Nan::New<String>(matches[i]).ToLocalChecked());
                free(matches[i]);
            }
            free(matches);
        }
        info.GetReturnValue().Set(result);
    } else {
        throw_aug_error_msg(obj->m_aug);
        Nan::Undefined();
    }
}

/*
 * Wrapper of aug_print().
 * Returns an object of key/value matching given path expression
 */
NAN_METHOD(LibAugeas::print) {
    Nan::HandleScope scope;

    if (info.Length() != 1) {
        Nan::ThrowError("Function expects incl argument");
        Nan::Undefined();
    }

    LibAugeas *obj = node::ObjectWrap::Unwrap<LibAugeas>(info.This());
    String::Utf8Value incl(isol(), info[0]);
    Local<Object> res = Nan::New<Object>();

    std::string matchPath = "/files" + std::string(*incl) + "/*";
    FILE *out = tmpfile();
    if (aug_print(obj->m_aug, out, matchPath.c_str()) == 0) {
        char line[256];
        rewind(out);
        while (fgets(line, 256, out) != NULL) {
            // remove end of line
            line[strlen(line) - 1] = '\0';
            std::string s = line;
            ;
            // skip comments
            if (s.find("#comment") != std::string::npos)
                continue;
            s = s.substr(matchPath.length() - 1);
            // split by '=' sign
            size_t eqpos = s.find(" = ");
            if (eqpos == std::string::npos)
                continue;
            // extract key and value
            std::string key = s.substr(0, eqpos);
            std::string value = s.substr(eqpos + 3);
            // remove '"' sign from around value
            value.erase(value.begin());
            value.erase(value.end() - 1);
            res->Set(ctx(),
                     Nan::New<String>(key).ToLocalChecked(),
                     Nan::New<String>(value).ToLocalChecked());
        }

        fclose(out);
        info.GetReturnValue().Set(res);
    }

    fclose(out);
    Nan::Undefined();
}

/*
 * Wrapper of aug_load() - load /files
 */
NAN_METHOD(LibAugeas::load) {
    Nan::HandleScope scope;

    if (info.Length() != 0) {
        Nan::ThrowError("Function does not accept arguments");
        Nan::Undefined();
    }

    LibAugeas *obj = node::ObjectWrap::Unwrap<LibAugeas>(info.This());

    /*
     * aug_load() returns -1 on error, 0 on success. Success includes the case
     * where some files could not be loaded. Details of such files can be found
     * as '/augeas//error'.
     */
    int rc = aug_load(obj->m_aug);
    if (AUG_NOERROR != rc) {
        Nan::ThrowError("Failed to load files");
    }

    Nan::Undefined();
}

/*
 * Wrapper of aug_srun() - run augeas commands (like augtool does)
 * Returns the number of executed commands.
 * Throws expression on error or if the 'quit' command encountered.
 * Arguments:
 * string or array of strings
 */
NAN_METHOD(LibAugeas::srun) {
    Nan::HandleScope scope;

    if (info.Length() != 1) {
        Nan::ThrowError("Function accepts exactly one argument");
        Nan::Undefined();
    }

    LibAugeas *obj = node::ObjectWrap::Unwrap<LibAugeas>(info.This());

    std::string text;

    if (info[0]->IsArray()) {
        text = join(Local<Array>::Cast(info[0]));
    } else {
        String::Utf8Value t_str(isol(), info[0]);
        text = *t_str;
    }

    /*
     * Returns the number of executed commands on success,
     * -1 on failure, and -2 if a 'quit' command was encountered.
     * TODO: use output (the second argument to aug_srun() != NULL)
     */
    int rc = aug_srun(obj->m_aug, NULL, text.c_str());
    if (rc >= 0) {
        info.GetReturnValue().Set(Nan::New<Number>(rc));
    } else if (-1 == rc) {
        throw_aug_error_msg(obj->m_aug);
    } else if (-2 == rc) {
        Nan::ThrowError("'quit' command was encountered");
    } else {
        Nan::ThrowError("Unexpected return code from aug_srun()");
    }

    Nan::Undefined();
}

LibAugeas::LibAugeas() : m_aug(NULL) {}

LibAugeas::~LibAugeas() { aug_close(m_aug); }

struct CreateAugeasUV {
    uv_work_t request;
    Nan::Callback callback;
    std::string root;
    std::string loadpath;
    std::string lens;
    std::string incl;
    std::string excl;
    std::string srun;
    unsigned int flags;
    augeas *aug;
};

/*
 * This function should immediately return if any call to augeas API fails.
 * The caller should check aug_error() before doing anything.
 */
void createAugeasWork(uv_work_t *req) {
    int rc = AUG_NOERROR;

    CreateAugeasUV *her = static_cast<CreateAugeasUV *>(req->data);

    // do not load all lenses if a specific lens is given,
    // ignore any setting in flags.
    // XXX: AUG_NO_MODL_AUTOLOAD implies AUG_NO_LOAD
    if (!her->lens.empty()) {
        her->flags |= AUG_NO_MODL_AUTOLOAD;
    }

    her->aug = aug_init(her->root.c_str(), her->loadpath.c_str(), her->flags);
    rc = aug_error(her->aug);
    if (AUG_NOERROR != rc)
        return;

    /*
     * Consider lens/incl/excl interface obsolete.
     * With srun: respect all flags (AUG_NO_MODL_AUTOLOAD, AUG_NO_LOAD),
     * execute srun commands and return.
     */
    if (!her->srun.empty()) {
        rc = aug_srun(her->aug, NULL, her->srun.c_str());
        return;
    }

    if (!her->lens.empty()) {
        // specifying which lens to load
        // /augeas/load/<random-name>/lens = e. g.: "hosts.lns" or
        // "@Hosts_Access"
        std::string basePath =
            "/augeas/load/1"; // "1" is a random valid name :-)
        std::string lensPath = basePath + "/lens";
        std::string lensVal = her->lens;
        if ((lensVal[0] != '@') // if not a module
            && (lensVal.rfind(".lns") == std::string::npos)) {
            lensVal += ".lns";
        }
        rc = aug_set(her->aug, lensPath.c_str(), lensVal.c_str());
        if (AUG_NOERROR != rc)
            return;

        if (!her->incl.empty()) {
            // specifying which files to load :
            // /augeas/load/<random-name>/incl = glob
            std::string inclPath = basePath + "/incl";
            rc = aug_set(her->aug, inclPath.c_str(), her->incl.c_str());
            if (AUG_NOERROR != rc)
                return;
        }
        if (!her->excl.empty()) {
            // specifying which files NOT to load
            // /augeas/load/<random-name>/excl = glob (e. g. "*.dpkg-new")
            std::string exclPath = basePath + "/excl";
            rc = aug_set(her->aug, exclPath.c_str(), her->excl.c_str());
            if (AUG_NOERROR != rc)
                return;
        }

        rc = aug_load(her->aug);
        if (AUG_NOERROR != rc)
            return;
    }
}

void createAugeasAfter(uv_work_t *req) {
    Nan::HandleScope scope;

    CreateAugeasUV *her = static_cast<CreateAugeasUV *>(req->data);
    Local<Value> argv[] = { LibAugeas::New(her->aug) };

    Nan::TryCatch try_catch;
    her->callback.Call(1, argv);
    delete her;
    if (try_catch.HasCaught()) {
        Nan::FatalException(try_catch);
    }
}

/*
 * Creates an Augeas object from JS side either in sync or async way
 * depending on the last argument:
 *
 * augeas.createAugeas([...], function(aug) {...}) - async
 *
 * or:
 *
 * var aug = augeas.createAugeas([...]) - sync
 *
 */
NAN_METHOD(createAugeas) {
    Nan::HandleScope scope;

    // options for aug_init(root, loadpath, flags):
    std::string root;
    std::string loadpath;
    unsigned int flags = 0;

    // Allow passing options as an JS object:
    if (info[0]->IsObject()) {
        Local<Object> obj = info[0]->ToObject(ctx()).ToLocalChecked();
        root = memberToString(obj, "root");
        loadpath = memberToString(obj, "loadpath");
        flags = memberToUint32(obj, "flags");
    } else {
        // C-like way:
        if (info[0]->IsString()) {
            String::Utf8Value p_str(isol(), info[0]);
            root = *p_str;
        }
        if (info[1]->IsString()) {
            String::Utf8Value l_str(isol(), info[1]);
            loadpath = *l_str;
        }
        if (info[2]->IsNumber()) {
            flags = info[2]->Uint32Value(ctx()).ToChecked();
        }
    }

    // always set to be able to get error messages, if aug_init() failed:
    flags |= AUG_NO_ERR_CLOSE;

    /*
     * If the last argument is a function, create augeas
     * in an async way, and then pass it to that function.
     */
    bool async = (info.Length() > 0) && (info[info.Length() - 1]->IsFunction());

    if (async) {
        CreateAugeasUV *her = new CreateAugeasUV();
        her->request.data = her;
        her->callback.SetFunction(
            Local<Function>::Cast(info[info.Length() - 1]));

        her->root = root;
        her->loadpath = loadpath;
        her->flags = flags;

        // Extra options for async mode:
        if (info[0]->IsObject()) {
            Local<Object> obj = info[0]->ToObject(ctx()).ToLocalChecked();
            her->lens = memberToString(obj, "lens");
            her->incl = memberToString(obj, "incl");
            her->excl = memberToString(obj, "excl");

            Local<Value> srun =
                obj->Get(ctx(), Nan::New<String>("srun").ToLocalChecked()).ToLocalChecked();
            if (srun->IsArray()) {
                her->srun = join(Local<Array>::Cast(srun));
            } else {
                her->srun = memberToString(obj, "srun");
            }
        }

        uv_queue_work(uv_default_loop(), &her->request, createAugeasWork,
                      (uv_after_work_cb)createAugeasAfter);

        Nan::Undefined();
    } else { // sync

        augeas *aug = aug_init(root.c_str(), loadpath.c_str(), flags);

        if (NULL == aug) { // should not happen due to AUG_NO_ERR_CLOSE
            Nan::ThrowError("aug_init() badly failed: it should not return "
                            "NULL, but it did.");
            Nan::Undefined();
        } else if (AUG_NOERROR != aug_error(aug)) {
            throw_aug_error_msg(aug);
            aug_close(aug);
            Nan::Undefined();
        }

        info.GetReturnValue().Set(LibAugeas::New(aug));
    }
}

void init(Handle<Object> target) {
    LibAugeas::Init(target);

    target->Set(ctx(),
		Nan::New<String>("createAugeas").ToLocalChecked(),
                Nan::New<FunctionTemplate>(createAugeas)->GetFunction(ctx()).ToLocalChecked());
}

NODE_MODULE(augeas, init)