/*
 * augeas.c: Ruby bindings for augeas
 *
 * Copyright (C) 2008-2011 Red Hat Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
 *
 * Author: Bryan Kearney <bkearney@redhat.com>
 */
#include "_augeas.h"

#include <ruby.h>
#include <augeas.h>

static VALUE c_augeas;
static VALUE c_facade;

static augeas *aug_handle(VALUE s) {
    augeas *aug;

    Data_Get_Struct(s, struct augeas, aug);
    if (aug == NULL) {
        rb_raise(rb_eSystemCallError, "Failed to retrieve connection");
    }
    return aug;
}

static void augeas_free(augeas *aug) {
    if (aug != NULL)
        aug_close(aug);
}

/*
 * call-seq:
 *   get(PATH) -> String
 *
 * Lookup the value associated with PATH
 */
VALUE augeas_get(VALUE s, VALUE path) {
    augeas *aug = aug_handle(s);
    const char *cpath = StringValueCStr(path);
    const char *value = NULL;

    int r = aug_get(aug, cpath, &value);
    /* There used to be a bug in Augeas that would make it not properly set
     * VALUE to NULL when PATH was invalid. We check RETVAL, too, to avoid
     * running into that */
    if (r == 1 && value != NULL) {
        return rb_str_new(value, strlen(value)) ;
    } else {
        return Qnil;
    }
}

/*
 * call-seq:
 *   get(PATH) -> String
 *
 * Lookup the value associated with PATH
 */
VALUE facade_get(VALUE s, VALUE path) {
    augeas *aug = aug_handle(s);
    const char *cpath = StringValueCStr(path);
    const char *value = NULL;

    int retval = aug_get(aug, cpath, &value);

    if (retval == 1 && value != NULL) {
        return rb_str_new(value, strlen(value));
    } else {
        return Qnil;
    }
}

/*
 * call-seq:
 *   exists(PATH) -> boolean
 *
 * Return true if there is an entry for this path, false otherwise
 */
VALUE augeas_exists(VALUE s, VALUE path) {
    augeas *aug = aug_handle(s);
    const char *cpath = StringValueCStr(path);
    int ret = aug_get(aug, cpath, NULL);

    return (ret == 1) ? Qtrue : Qfalse;
}

static int set(VALUE s, VALUE path, VALUE value) {
    augeas *aug = aug_handle(s);
    const char *cpath = StringValueCStr(path) ;
    const char *cvalue = StringValueCStrOrNull(value) ;

    return aug_set(aug, cpath, cvalue) ;
}

/*
 * call-seq:
 *   set(PATH, VALUE) -> int
 *
 * Set the value associated with PATH to VALUE. VALUE is copied into the
 * internal data structure. Intermediate entries are created if they don't
 * exist.
 */
VALUE augeas_set(VALUE s, VALUE path, VALUE value) {
    int callValue = set(s, path, value);

    return (callValue == 0) ? Qtrue : Qfalse;
}

VALUE facade_set(VALUE s, VALUE path, VALUE value) {
    return INT2FIX(set(s, path, value));
}

/*
 * call-seq:
 *   setm(BASE, SUB, VALUE) -> boolean
 *
 *  Set multiple nodes in one operation. Find or create a node matching
 *  SUB by interpreting SUB as a path expression relative to each node
 *  matching BASE. SUB may be NULL, in which case all the nodes matching
 *  BASE will be modified.
 */
VALUE augeas_setm(VALUE s, VALUE base, VALUE sub, VALUE value) {
    augeas *aug = aug_handle(s);
    const char *cbase = StringValueCStr(base) ;
    const char *csub = StringValueCStrOrNull(sub) ;
    const char *cvalue = StringValueCStrOrNull(value) ;

    int callValue = aug_setm(aug, cbase, csub, cvalue) ;
    return INT2FIX(callValue);
}

/*
 * call-seq:
 *   insert(PATH, LABEL, BEFORE) -> int
 *
 * Make LABEL a sibling of PATH by inserting it directly before or after PATH.
 * The boolean BEFORE determines if LABEL is inserted before or after PATH.
 */
VALUE augeas_insert(VALUE s, VALUE path, VALUE label, VALUE before) {
    augeas *aug = aug_handle(s);
    const char *cpath = StringValueCStr(path) ;
    const char *clabel = StringValueCStr(label) ;

    int callValue = aug_insert(aug, cpath, clabel, RTEST(before));
    return INT2FIX(callValue) ;
}

/*
 * call-seq:
 *   mv(SRC, DST) -> int
 *
 * Move the node SRC to DST. SRC must match exactly one node in the
 * tree. DST must either match exactly one node in the tree, or may not
 * exist yet. If DST exists already, it and all its descendants are
 * deleted. If DST does not exist yet, it and all its missing ancestors are
 * created.
 */
VALUE augeas_mv(VALUE s, VALUE src, VALUE dst) {
    augeas *aug = aug_handle(s);
    const char *csrc = StringValueCStr(src);
    const char *cdst = StringValueCStr(dst);
    int r = aug_mv(aug, csrc, cdst);

    return INT2FIX(r);
}

/*
 * call-seq:
 *   rm(PATH) -> int
 *
 * Remove path and all its children. Returns the number of entries removed
 */
VALUE augeas_rm(VALUE s, VALUE path) {
    augeas *aug = aug_handle(s);
    const char *cpath = StringValueCStr(path) ;

    int callValue = aug_rm(aug, cpath) ;
    return INT2FIX(callValue) ;
}

/*
 * call-seq:
 *       match(PATH) -> an_array
 *
 * Return all the paths that match the path expression PATH as an aray of
 * strings.
 */
VALUE augeas_match(VALUE s, VALUE p) {
    augeas *aug = aug_handle(s);
    const char *path = StringValueCStr(p);
    VALUE result;
    char **matches = NULL;
    int cnt, i;

    cnt = aug_match(aug, path, &matches) ;
    if (cnt < 0)
        rb_raise(rb_eSystemCallError, "Matching path expression '%s' failed",
                 path);

    result = rb_ary_new();
    for (i = 0; i < cnt; i++) {
        rb_ary_push(result, rb_str_new(matches[i], strlen(matches[i])));
        free(matches[i]) ;
    }
    free (matches) ;

    return result ;
}

/*
 * call-seq:
 *       match(PATH) -> an_array
 *
 * Return all the paths that match the path expression PATH as an aray of
 * strings.
 * Returns an empty array if no paths were found.
 */
VALUE facade_match(VALUE s, VALUE p) {
    augeas *aug = aug_handle(s);
    const char *path = StringValueCStr(p);
    char **matches = NULL;
    int cnt, i;
    VALUE result;

    cnt = aug_match(aug, path, &matches) ;
    if (cnt < 0)
        return -1;

    result = rb_ary_new();
    for (i = 0; i < cnt; i++) {
        rb_ary_push(result, rb_str_new(matches[i], strlen(matches[i])));
        free(matches[i]) ;
    }
    free (matches) ;

    return result ;
}

/*
 * call-seq:
 *       save() -> boolean
 *
 * Write all pending changes to disk
 */
VALUE augeas_save(VALUE s) {
    augeas *aug = aug_handle(s);

    return (aug_save(aug) == 0) ? Qtrue : Qfalse;
}

/*
 * call-seq:
 *       save() -> int
 *
 * Write all pending changes to disk
 */
VALUE facade_save(VALUE s) {
    augeas *aug = aug_handle(s);
    return INT2FIX(aug_save(aug));
}

/*
 * call-seq:
 *       load() -> boolean
 *
 * Load files from disk according to the transforms under +/augeas/load+
 */
VALUE augeas_load(VALUE s) {
    augeas *aug = aug_handle(s);
    int callValue = aug_load(aug);
    VALUE returnValue ;

    if (callValue == 0)
        returnValue = Qtrue ;
    else
        returnValue = Qfalse ;

    return returnValue ;
}

/*
 * call-seq:
 *   defvar(NAME, EXPR) -> boolean
 *
 * Define a variable NAME whose value is the result of evaluating EXPR. If
 * a variable NAME already exists, its name will be replaced with the
 * result of evaluating EXPR.
 *
 * If EXPR is NULL, the variable NAME will be removed if it is defined.
 *
 */
VALUE augeas_defvar(VALUE s, VALUE name, VALUE expr) {
    augeas *aug = aug_handle(s);
    const char *cname = StringValueCStr(name);
    const char *cexpr = StringValueCStrOrNull(expr);

    int r = aug_defvar(aug, cname, cexpr);

    return (r < 0) ? Qfalse : Qtrue;
}

/*
 * call-seq:
 *   defnode(NAME, EXPR, VALUE) -> boolean
 *
 * Define a variable NAME whose value is the result of evaluating EXPR,
 * which must be non-NULL and evaluate to a nodeset. If a variable NAME
 * already exists, its name will be replaced with the result of evaluating
 * EXPR.
 *
 * If EXPR evaluates to an empty nodeset, a node is created, equivalent to
 * calling AUG_SET(AUG, EXPR, VALUE) and NAME will be the nodeset containing
 * that single node.
 *
 * Returns +false+ if +aug_defnode+ fails, and the number of nodes in the
 * nodeset on success.
 */
VALUE augeas_defnode(VALUE s, VALUE name, VALUE expr, VALUE value) {
    augeas *aug = aug_handle(s);
    const char *cname = StringValueCStr(name);
    const char *cexpr = StringValueCStrOrNull(expr);
    const char *cvalue = StringValueCStrOrNull(value);

    /* FIXME: Figure out a way to return created, maybe accept a block
       that gets run when created == 1 ? */
    int r = aug_defnode(aug, cname, cexpr, cvalue, NULL);

    return (r < 0) ? Qfalse : INT2NUM(r);
}

static VALUE init(VALUE class, VALUE m, VALUE r, VALUE l, VALUE f) {
    unsigned int flags = NUM2UINT(f);
    const char *root = StringValueCStrOrNull(r);
    const char *loadpath = StringValueCStrOrNull(l);
    augeas *aug = NULL;

    aug = aug_init(root, loadpath, flags);
    if (aug == NULL) {
        rb_raise(rb_eSystemCallError, "Failed to initialize Augeas");
    }
    return Data_Wrap_Struct(class, NULL, augeas_free, aug);
}

VALUE augeas_init(VALUE m, VALUE r, VALUE l, VALUE f) {
    return init(c_augeas, m, r, l, f);
}

VALUE facade_init(VALUE m, VALUE r, VALUE l, VALUE f) {
    return init(c_facade, m, r, l, f);
}

VALUE augeas_close (VALUE s) {
    augeas *aug = aug_handle(s);

    aug_close(aug);
    DATA_PTR(s) = NULL;

    return Qnil;
}

static void hash_set(VALUE hash, const char *sym, VALUE v) {
    rb_hash_aset(hash, ID2SYM(rb_intern(sym)), v);
}

/*
 * call-seq:
 *   error -> HASH
 *
 * Retrieve details about the last error encountered and return those
 * details in a HASH with the following entries:
 * - :code    error code from +aug_error+
 * - :message error message from +aug_error_message+
 * - :minor   minor error message from +aug_minor_error_message+
 * - :details error details from +aug_error_details+
 */
VALUE augeas_error(VALUE s) {
    augeas *aug = aug_handle(s);
    int code;
    const char *msg;
    VALUE result;

    result = rb_hash_new();

    code = aug_error(aug);
    hash_set(result, "code", INT2NUM(code));

    msg = aug_error_message(aug);
    if (msg != NULL)
        hash_set(result, "message", rb_str_new2(msg));

    msg = aug_error_minor_message(aug);
    if (msg != NULL)
        hash_set(result, "minor", rb_str_new2(msg));

    msg = aug_error_details(aug);
    if (msg != NULL)
        hash_set(result, "details", rb_str_new2(msg));

    return result;
}

static void hash_set_range(VALUE hash, const char *sym,
                           unsigned int start, unsigned int end) {
    VALUE r;

    r = rb_range_new(INT2NUM(start), INT2NUM(end), 0);
    hash_set(hash, sym, r);
}

VALUE augeas_span(VALUE s, VALUE path) {
    augeas *aug = aug_handle(s);
    char *cpath = StringValueCStr(path);
    char *filename = NULL;
    unsigned int label_start, label_end, value_start, value_end,
        span_start, span_end;
    int r;
    VALUE result;

    r = aug_span(aug, cpath,
                 &filename,
                 &label_start, &label_end,
                 &value_start, &value_end,
                 &span_start, &span_end);

    result = rb_hash_new();

    if (r == 0) {
        hash_set(result, "filename", rb_str_new2(filename));
        hash_set_range(result, "label", label_start, label_end);
        hash_set_range(result, "value", value_start, value_end);
        hash_set_range(result, "span", span_start, span_end);
    }

    free(filename);

    return result;
}

/*
 * call-seq:
 *   srun(COMMANDS) -> [int, String]
 *
 * Run one or more newline-separated commands, returning their output.
 *
 * Returns:
 * an array where the first element is the number of executed commands on
 * success, -1 on failure, and -2 if a 'quit' command was encountered.
 * The second element is a string of the output from all commands.
 */
VALUE augeas_srun(VALUE s, VALUE text) {
    augeas *aug = aug_handle(s);
    const char *ctext = StringValueCStr(text);
    int r;
    VALUE result;
    struct memstream ms;
    __aug_init_memstream(&ms);

    r = aug_srun(aug, ms.stream, ctext);
    __aug_close_memstream(&ms);

    result = rb_ary_new();
    rb_ary_push(result, INT2NUM(r));
    rb_ary_push(result, rb_str_new2(ms.buf));

    free(ms.buf);
    return result;
}

/*
 * call-seq:
 *   label(PATH) -> String
 *
 * Lookup the label associated with PATH
 */
VALUE augeas_label(VALUE s, VALUE path) {
    augeas *aug = aug_handle(s);
    const char *cpath = StringValueCStr(path);
    const char *label;

    aug_label(aug, cpath, &label);
    if (label != NULL) {
        return rb_str_new(label, strlen(label)) ;
    } else {
        return Qnil;
    }
}

/*
 * call-seq:
 *   rename(SRC, LABEL) -> int
 *
 * Rename the label of all nodes matching SRC to LABEL.
 *
 * Returns +false+ if +aug_rename+ fails, and the number of nodes renamed
 * on success.
 */
VALUE augeas_rename(VALUE s, VALUE src, VALUE label) {
    augeas *aug = aug_handle(s);
    const char *csrc = StringValueCStr(src);
    const char *clabel = StringValueCStr(label);
    int r = aug_rename(aug, csrc, clabel);

    return (r < 0) ? Qfalse : INT2NUM(r);
}

/*
 * call-seq:
 *   text_store(LENS, NODE, PATH) -> boolean
 *
 * Use the value of node NODE as a string and transform it into a tree
 * using the lens LENS and store it in the tree at PATH, which will be
 * overwritten. PATH and NODE are path expressions.
 */
VALUE augeas_text_store(VALUE s, VALUE lens, VALUE node, VALUE path) {
    augeas *aug = aug_handle(s);
    const char *clens = StringValueCStr(lens);
    const char *cnode = StringValueCStr(node);
    const char *cpath = StringValueCStr(path);
    int r = aug_text_store(aug, clens, cnode, cpath);

    return (r < 0) ? Qfalse : Qtrue;
}

/*
 * call-seq:
 *   text_retrieve(LENS, NODE_IN, PATH, NODE_OUT) -> boolean
 *
 * Transform the tree at PATH into a string using lens LENS and store it in
 * the node NODE_OUT, assuming the tree was initially generated using the
 * value of node NODE_IN. PATH, NODE_IN, and NODE_OUT are path expressions.
 */
VALUE augeas_text_retrieve(VALUE s, VALUE lens, VALUE node_in, VALUE path, VALUE node_out) {
    augeas *aug = aug_handle(s);
    const char *clens = StringValueCStr(lens);
    const char *cnode_in = StringValueCStr(node_in);
    const char *cpath = StringValueCStr(path);
    const char *cnode_out = StringValueCStr(node_out);
    int r = aug_text_retrieve(aug, clens, cnode_in, cpath, cnode_out);

    return (r < 0) ? Qfalse : Qtrue;
}

void Init__augeas() {

    /* Define the ruby class */
    c_augeas = rb_define_class("Augeas", rb_cObject) ;
    rb_undef_alloc_func(c_augeas);
    c_facade = rb_define_class_under(c_augeas, "Facade", rb_cObject);
    rb_undef_alloc_func(c_facade);

    /* Constants for enum aug_flags */
#define DEF_AUG_FLAG(name) \
    rb_define_const(c_augeas, #name, INT2NUM(AUG_##name))
    DEF_AUG_FLAG(NONE);
    DEF_AUG_FLAG(SAVE_BACKUP);
    DEF_AUG_FLAG(SAVE_NEWFILE);
    DEF_AUG_FLAG(TYPE_CHECK);
    DEF_AUG_FLAG(NO_STDINC);
    DEF_AUG_FLAG(SAVE_NOOP);
    DEF_AUG_FLAG(NO_LOAD);
    DEF_AUG_FLAG(NO_MODL_AUTOLOAD);
    DEF_AUG_FLAG(ENABLE_SPAN);
#undef DEF_AUG_FLAG

    /* Constants for enum aug_errcode_t */
#define DEF_AUG_ERR(name) \
    rb_define_const(c_augeas, #name, INT2NUM(AUG_##name))
    DEF_AUG_ERR(NOERROR);
    DEF_AUG_ERR(ENOMEM);
    DEF_AUG_ERR(EINTERNAL);
    DEF_AUG_ERR(EPATHX);
    DEF_AUG_ERR(ENOMATCH);
    DEF_AUG_ERR(EMMATCH);
    DEF_AUG_ERR(ESYNTAX);
    DEF_AUG_ERR(ENOLENS);
    DEF_AUG_ERR(EMXFM);
    DEF_AUG_ERR(ENOSPAN);
    DEF_AUG_ERR(EMVDESC);
    DEF_AUG_ERR(ECMDRUN);
    DEF_AUG_ERR(EBADARG);
    DEF_AUG_ERR(ELABEL);
#undef DEF_AUG_ERR

    /* Define the methods */
    rb_define_singleton_method(c_augeas, "open3", augeas_init, 3);
    rb_define_method(c_augeas, "defvar", augeas_defvar, 2);
    rb_define_method(c_augeas, "defnode", augeas_defnode, 3);
    rb_define_method(c_augeas, "get", augeas_get, 1);
    rb_define_method(c_augeas, "exists", augeas_exists, 1);
    rb_define_method(c_augeas, "insert", augeas_insert, 3);
    rb_define_method(c_augeas, "mv", augeas_mv, 2);
    rb_define_method(c_augeas, "rm", augeas_rm, 1);
    rb_define_method(c_augeas, "match", augeas_match, 1);
    rb_define_method(c_augeas, "save", augeas_save, 0);
    rb_define_method(c_augeas, "load", augeas_load, 0);
    rb_define_method(c_augeas, "set_internal", augeas_set, 2);
    rb_define_method(c_augeas, "setm", augeas_setm, 3);
    rb_define_method(c_augeas, "close", augeas_close, 0);
    rb_define_method(c_augeas, "error", augeas_error, 0);
    rb_define_method(c_augeas, "span", augeas_span, 1);
    rb_define_method(c_augeas, "srun", augeas_srun, 1);
    rb_define_method(c_augeas, "label", augeas_label, 1);
    rb_define_method(c_augeas, "rename", augeas_rename, 2);
    rb_define_method(c_augeas, "text_store", augeas_text_store, 3);
    rb_define_method(c_augeas, "text_retrieve", augeas_text_retrieve, 4);

    /* Define methods to support the 'new' API in Augeas::Facade */
    rb_define_singleton_method(c_facade, "open3", facade_init, 3);
    /* The `close` and `error` methods as used unchanged in the ruby bindings */
    rb_define_method(c_facade, "close", augeas_close, 0);
    rb_define_method(c_facade, "error", augeas_error, 0);
    rb_define_method(c_facade, "augeas_defvar", augeas_defvar, 2);
    rb_define_method(c_facade, "augeas_defnode", augeas_defnode, 3);
    rb_define_method(c_facade, "augeas_get", augeas_get, 1);
    rb_define_method(c_facade, "augeas_exists", augeas_exists, 1);
    rb_define_method(c_facade, "augeas_insert", augeas_insert, 3);
    rb_define_method(c_facade, "augeas_mv", augeas_mv, 2);
    rb_define_method(c_facade, "augeas_rm", augeas_rm, 1);
    rb_define_method(c_facade, "augeas_match", facade_match, 1);
    rb_define_method(c_facade, "augeas_save", facade_save, 0);
    rb_define_method(c_facade, "augeas_load", augeas_load, 0);
    rb_define_method(c_facade, "augeas_set", facade_set, 2);
    rb_define_method(c_facade, "augeas_setm", augeas_setm, 3);
    rb_define_method(c_facade, "augeas_span", augeas_span, 1);
    rb_define_method(c_facade, "augeas_srun", augeas_srun, 1);
    rb_define_method(c_facade, "augeas_label", augeas_label, 1);
    rb_define_method(c_facade, "augeas_rename", augeas_rename, 2);
    rb_define_method(c_facade, "augeas_text_store", augeas_text_store, 3);
    rb_define_method(c_facade, "augeas_text_retrieve", augeas_text_retrieve, 4);
}

/*
 * Local variables:
 *  indent-tabs-mode: nil
 *  c-indent-level: 4
 *  c-basic-offset: 4
 *  tab-width: 4
 * End:
 */
