#ifndef PZK_XS_UTILS_H_
#define PZK_XS_UTILS_H_
#include <stdarg.h>

void* _tied_object_to_ptr(pTHX_ SV* obj_sv, const char* var, const char* pkg, int unsafe) {
    if (!SvROK(obj_sv) || (SvTYPE(SvRV(obj_sv)) != SVt_PVHV)) {
        if (unsafe) return NULL;
        Perl_croak(aTHX_ "%s is not a blessed reference of type %s", var, pkg);
    }

    SV* tied_hash = SvRV(obj_sv);
    MAGIC* ext_magic = mg_find(tied_hash, PERL_MAGIC_ext);
    if (!ext_magic) {
        if (unsafe) return NULL;
        Perl_croak(aTHX_ "%s has not been initialized by %s", var, pkg);
    }

    return  (void*) ext_magic->mg_ptr;
}

void* unsafe_tied_object_to_ptr(pTHX_ SV* obj_sv) {
    return _tied_object_to_ptr(aTHX_ obj_sv, NULL, NULL, 1);
}

void* tied_object_to_ptr(pTHX_ SV* obj_sv, const char* var, const char* pkg) {
    return _tied_object_to_ptr(aTHX_ obj_sv, var, pkg, 0);
}

SV* ptr_to_tied_object(pTHX_ void* ptr, const char* pkg) {
    HV* stash = gv_stashpv(pkg, GV_ADDWARN);
    SV* attr_hash = (SV*) newHV();
    sv_magic(attr_hash, Nullsv, PERL_MAGIC_ext, (const char*) ptr, 0);
    return sv_bless(newRV_noinc(attr_hash), stash);
}

struct ACL* sv_to_acl_entry(pTHX_ SV* acl_sv) {
    if (!SvROK(acl_sv) || (SvTYPE(SvRV(acl_sv)) != SVt_PVHV))
        Perl_croak(aTHX_ "acl entry must be a hash ref");
    HV* acl_hv = (HV*) SvRV(acl_sv);
    struct ACL* acl_entry; Newxz(acl_entry, 1, struct ACL);

    SV** perm_val_ptr = hv_fetch(acl_hv, "perms", 5, 0);
    if (perm_val_ptr) acl_entry->perms = SvIV(*perm_val_ptr);

    SV** scheme_val_ptr = hv_fetch(acl_hv, "scheme", 6, 0);
    if (scheme_val_ptr) acl_entry->id.scheme = SvPV_nolen(*scheme_val_ptr);

    SV** id_val_ptr = hv_fetch(acl_hv, "id", 2, 0);
    if (id_val_ptr) acl_entry->id.id = SvPV_nolen(*id_val_ptr);

    return acl_entry;
}

struct ACL_vector* sv_to_acl_vector(pTHX_ SV* acl_v_sv) {
    if (!SvROK(acl_v_sv) || !(SvTYPE(SvRV(acl_v_sv)) == SVt_PVAV))
        Perl_croak(aTHX_ "acl must be an array ref of hash refs");
    AV* acl_v_av = (AV*) SvRV(acl_v_sv);
    SSize_t length = av_len(acl_v_av) + 1;

    struct ACL_vector *v;
    Newxz(v, 1, struct ACL_vector);
    Newxz(v->data, length, struct ACL);
    int i; for (i = 0; i < length; i++) {
        SV* acl_sv = *(av_fetch(acl_v_av, i, 0));
        v->data[i] = *(sv_to_acl_entry(aTHX_ acl_sv));
    }
    v->count = length;

    return v;
}

SV* acl_entry_to_sv(pTHX_ struct ACL* acl_entry) {
    HV* acl_hv = newHV();

    hv_store(acl_hv, "perms", 5, newSViv(acl_entry->perms), 0);
    hv_store(acl_hv, "scheme", 6, newSVpv(acl_entry->id.scheme, 0), 0);
    hv_store(acl_hv, "id", 2, newSVpv(acl_entry->id.id, 0), 0);

    return newRV_noinc((SV*) acl_hv);
}

SV* acl_vector_to_sv(pTHX_ struct ACL_vector* acl_v) {
    AV* acl_v_av = newAV();
    int32_t length = acl_v->count;

    int i; for (i = 0; i < length; i++) {
        struct ACL* acl_entry = &acl_v->data[i];
        av_push(acl_v_av, acl_entry_to_sv(aTHX_ acl_entry));
    }

    return newRV_noinc((SV*) acl_v_av);
}

SV* stat_to_sv(pTHX_ struct Stat* stat) {
    HV* stat_hv = newHV();

    hv_store(stat_hv, "czxid",          5,  newSViv(stat->czxid),          0);
    hv_store(stat_hv, "mzxid",          5,  newSViv(stat->mzxid),          0);
    hv_store(stat_hv, "ctime",          5,  newSViv(stat->ctime),          0);
    hv_store(stat_hv, "mtime",          5,  newSViv(stat->mtime),          0);
    hv_store(stat_hv, "version",        7,  newSViv(stat->version),        0);
    hv_store(stat_hv, "cversion",       8,  newSViv(stat->cversion),       0);
    hv_store(stat_hv, "aversion",       8,  newSViv(stat->aversion),       0);
    hv_store(stat_hv, "ephemeralOwner", 14, newSViv(stat->ephemeralOwner), 0);
    hv_store(stat_hv, "dataLength",     10, newSViv(stat->dataLength),     0);
    hv_store(stat_hv, "numChildren",    11, newSViv(stat->numChildren),    0);
    hv_store(stat_hv, "pzxid",          5,  newSViv(stat->pzxid),          0);

    return newRV_noinc((SV*) stat_hv);
}

SV* event_to_sv(pTHX_ pzk_event_t* event) {
    HV* event_hv = newHV();

    hv_store(event_hv, "type",    4, newSViv(event->type),           0);
    hv_store(event_hv, "state",   5, newSViv(event->state),          0);
    hv_store(event_hv, "path",    4, newSVpv(event->path, 0),        0);
    hv_store(event_hv, "watcher", 7, SvREFCNT_inc((SV*) event->arg), 0);

    return newRV_noinc((SV*) event_hv);
}

pzk_event_t* sv_to_event(pTHX_ void* watcher, SV* event_sv) {
    if (!SvROK(event_sv) || (SvTYPE(SvRV(event_sv)) != SVt_PVHV))
        Perl_croak(aTHX_ "event must be a hash ref");
    HV* event_hv = (HV*) SvRV(event_sv);

    SV** type_val_ptr = hv_fetch(event_hv, "type", 4, 0);
    int type = type_val_ptr ? SvIV(*type_val_ptr) : -1;

    SV** state_val_ptr = hv_fetch(event_hv, "state", 5, 0);
    int state = state_val_ptr ? SvIV(*state_val_ptr) : -1;

    SV** path_val_ptr= hv_fetch(event_hv, "path", 4, 0);
    char* path = path_val_ptr ? SvPV_nolen(*path_val_ptr) : NULL;

    return new_pzk_event(type, state, path, watcher);
}

void throw_zerror(pTHX_ int rc, const char* fmt, ...) {
    dSP;

    ENTER;
    SAVETMPS;

    HV* args_hv = newHV();
    hv_store(args_hv, "code", 4, newSViv(rc), 0);

    va_list args; va_start(args, fmt);
    hv_store(args_hv, "message", 7, newSVsv(vmess(fmt, &args)), 0);
    va_end(args);

    SV* args_sv = newRV_noinc((SV*) args_hv);

    PUSHMARK(SP);
    XPUSHs(sv_2mortal(newSVpv("ZooKeeper::Error", 0)));
    XPUSHs(sv_2mortal(args_sv));
    PUTBACK;

    call_method("throw", G_DISCARD);

    FREETMPS;
    LEAVE;
}

SV* op_error_to_sv(pTHX_ zoo_op_result_t result) {
    HV* result_hv = newHV();

    hv_store(result_hv, "type",    4, newSVpv("error", 0),            0);
    hv_store(result_hv, "code",    4, newSViv(result.err),            0);
    hv_store(result_hv, "message", 7, newSVpv(zerror(result.err), 0), 0);

    return newRV_noinc((SV*) result_hv);
}

SV* op_to_sv(pTHX_ const zoo_op_t op) {
    HV* op_hv = newHV();

    if (op.type == ZOO_CREATE_OP) {
        hv_store(op_hv, "type", 4, newSVpv("create", 0), 0);
        hv_store(op_hv, "path", 4, newSVpv(op.create_op.buf, 0), 0);
    } else if (op.type == ZOO_DELETE_OP) {
        hv_store(op_hv, "type", 4, newSVpv("delete", 0), 0);
    } else if (op.type == ZOO_SETDATA_OP) {
        hv_store(op_hv, "type", 4, newSVpv("set", 0), 0);
        SV* stat_sv = stat_to_sv(aTHX_ op.set_op.stat);
        hv_store(op_hv, "stat", 4, stat_sv, 0);
    } else if (op.type == ZOO_CHECK_OP) {
        hv_store(op_hv, "type", 4, newSVpv("check", 0), 0);
    }

    return newRV_noinc((SV*) op_hv);
}

zoo_op_t sv_to_op(pTHX_ SV* const op_sv) {
    if (!SvROK(op_sv) || (SvTYPE(SvRV(op_sv)) != SVt_PVAV))
        Perl_croak(aTHX_ "op must be an array ref");
    AV* op_av = (AV*) SvRV(op_sv);
    SSize_t length = av_len(op_av) + 1;
    int type       = SvIV(*(av_fetch(op_av, 0, 0)));

    zoo_op_t op;
    if (type == ZOO_CREATE_OP) {
        size_t value_len = -1;
        SV* value_sv     = *(av_fetch(op_av, 2, 0));
        char* value      = SvOK(value_sv) ? SvPV(value_sv, value_len) : NULL;

        const char* path  = SvPV_nolen(*(av_fetch(op_av, 1, 0)));
        int buffer_len    = SvIV(*(av_fetch(op_av, 3, 0)));
        const struct ACL_vector* acl =
            sv_to_acl_vector(aTHX_ *(av_fetch(op_av, 4, 0)));
        int flags = SvIV(*(av_fetch(op_av, 5, 0)));

        char* buffer; Newxz(buffer, buffer_len + 1, char);

        zoo_create_op_init(&op, path, value, value_len, acl, flags, buffer, buffer_len);
    } else if (type == ZOO_DELETE_OP) {
        const char* path = SvPV_nolen(*(av_fetch(op_av, 1, 0)));
        int version      = SvIV(*(av_fetch(op_av, 2, 0)));
        zoo_delete_op_init(&op, path, version);
    } else if (type == ZOO_SETDATA_OP) {
        size_t value_len = -1;
        SV* value_sv     = *(av_fetch(op_av, 2, 0));
        char* value      = SvOK(value_sv) ? SvPV(value_sv, value_len) : NULL;

        const char* path  = SvPV_nolen(*(av_fetch(op_av, 1, 0)));
        int version       = SvIV(*(av_fetch(op_av, 3, 0)));

        struct Stat* stat; Newxz(stat, 1, struct Stat);

        zoo_set_op_init(&op, path, value, value_len, version, stat);
    } else if (type == ZOO_CHECK_OP) {
        const char* path = SvPV_nolen(*(av_fetch(op_av, 1, 0)));
        int version      = SvIV(*(av_fetch(op_av, 2, 0)));
        zoo_check_op_init(&op, path, version);
    }

    return op;
}

zoo_op_t* sv_to_ops(pTHX_ const SV* ops_sv) {
    if (!SvROK(ops_sv) || !(SvTYPE(SvRV(ops_sv)) == SVt_PVAV))
        Perl_croak(aTHX_ "ops must be an array ref");
    AV* ops_av     = (AV*) SvRV(ops_sv);
    SSize_t length = av_len(ops_av) + 1;

    zoo_op_t* ops; Newxz(ops, length, zoo_op_t);
    int i; for (i = 0; i < length; i++) {
        SV* op_sv = *(av_fetch(ops_av, i, 0));
        ops[i]    = sv_to_op(aTHX_ op_sv);
    }

    return ops;
}

void free_op(pTHX_ zoo_op_t op) {
    if (op.type == ZOO_CREATE_OP) {
        Safefree(op.create_op.buf);
        Safefree(op.create_op.acl->data);
        Safefree(op.create_op.acl);
    } else if (op.type == ZOO_SETDATA_OP) {
        Safefree(op.set_op.stat);
    }
}

void free_ops(pTHX_ zoo_op_t* ops, int length) {
    int i; for (i = 0; i < length; i++) {
        free_op(aTHX_ ops[i]);
    }
    Safefree(ops);
}

#endif // ifndef PZK_XS_UTILS_H_