The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"

#include "ppport.h"
#include <string.h>
#include <stdio.h>

#include "hiredis.h"
#include "net.h"
#include "sds.h"

typedef struct redhi_obj {
    redisContext *context;
    bool utf8;
} redhi_obj;

typedef redhi_obj *Redis__hiredis;

SV * _read_reply (Redis__hiredis self, redisReply *reply);
SV * _read_multi_bulk_reply (Redis__hiredis self, redisReply *reply);
SV * _read_bulk_reply (Redis__hiredis self, redisReply *reply);

SV * _read_reply (Redis__hiredis self, redisReply *reply) {
    if (reply->type == REDIS_REPLY_ARRAY) {
        return _read_multi_bulk_reply(self, reply);
    }
    else {
        if ( reply->type == REDIS_REPLY_ERROR ) 
          croak("%s",reply->str);
        return _read_bulk_reply(self, reply);
    }
}

SV * _read_multi_bulk_reply (Redis__hiredis self, redisReply *reply) {
    AV *arr_reply = newAV();
    SV *sv = newRV_noinc((SV*)arr_reply);
    int i;
    for ( i=0; i < reply->elements; i++) {
        av_push(arr_reply, _read_bulk_reply(self, reply->element[i]));
    }

    return sv;
}

SV * _read_bulk_reply (Redis__hiredis self, redisReply *reply) {
    SV *sv;

    if ( reply->type == REDIS_REPLY_STRING 
            || reply->type == REDIS_REPLY_STATUS 
            || reply->type == REDIS_REPLY_ERROR ) {
        sv = newSVpvn(reply->str,reply->len);
        if (self->utf8) {
            sv_utf8_decode(sv);
        }
    }
    else if ( reply->type == REDIS_REPLY_INTEGER ) {
        sv = newSViv(reply->integer);
    }
    else {
        // either REDIS_REPLY_NIL or something is awry
        sv = newSV(0);
    }

    return sv;
}

int _command_from_arr_ref (Redis__hiredis self, SV *cmd, char ***argv, size_t **argv_sizes) {
    AV *array;
    STRLEN len;
    int i;
    if ( SvTYPE(array = (AV*)SvRV(cmd))==SVt_PVAV ) {
        *argv = (char**)malloc((av_len(array) + 1) * sizeof(char *));
        *argv_sizes = (size_t*)malloc((av_len(array) + 1) * sizeof(size_t *));
        for ( i = 0; i < av_len(array)+1; i++ ) {
            SV **curr = av_fetch(array,i,0);
            if ( self->utf8 ) {
                (argv[0][i]) = SvPVutf8(*curr, len);
            }
            else {
                (argv[0][i]) = SvPV(*curr, len);
            }
            argv_sizes[0][i] = len;
        }
    }
    return i;
}

void assert_connected (redhi_obj *self) {
    if (self->context == NULL) {
        croak("%s","Not connected.");
    }
}

MODULE = Redis::hiredis PACKAGE = Redis::hiredis PREFIX = redis_hiredis_

void
redis_hiredis_connect(self, hostname, port = 6379)
    Redis::hiredis self
    char *hostname
    int port
    CODE:
        self->context = redisConnect(hostname, port);
        if ( self->context->err ) {
            croak("%s",self->context->errstr);
        }

void
redis_hiredis_connect_unix(self, path)
    Redis::hiredis self
    char *path
    CODE:
        self->context = redisConnectUnix(path);
        if ( self->context->err ) {
            croak("%s",self->context->errstr);
        }

SV *
redis_hiredis_command(self, ...)
    Redis::hiredis self
    PREINIT:
        int params;
        char **argv;
        size_t *argv_sizes;
        redisReply *reply;
    CODE:
        assert_connected(self);
        if ( items > 2 || SvROK(ST(1)) ) {
            if ( items > 2 ) {
                // because I am not sure how to pass the argument stack to another function,
                // lets just do our work here.
                params = items - 1;
                int i;
                STRLEN len;
                argv = malloc(params * sizeof(char *));
                argv_sizes = malloc(params * sizeof(size_t *));

                for ( i = 0; i < params; i++ ) {
                    if ( self->utf8 ) {
                        argv[i] = SvPVutf8(ST(i+1), len);
                    }
                    else {
                        argv[i] = SvPV(ST(i+1), len);
                    }
                    argv_sizes[i] = len;
                }
            }
            else {
                params = _command_from_arr_ref(self, ST(1), &argv, &argv_sizes);
            }
            reply  = redisCommandArgv(self->context, params, (const char**)argv, argv_sizes);
            free(argv);
            free(argv_sizes);
        }
        else {
            reply  = redisCommand(self->context, (char *)SvPV_nolen(ST(1)));
        }

        if(reply == NULL)
            croak("error processing command: %s\n", self->context->errstr);

        RETVAL = _read_reply(self, reply);
        freeReplyObject(reply);
    OUTPUT:
        RETVAL

void
redis_hiredis_append_command(self, cmd)
    Redis::hiredis self
    char *cmd
    CODE:
        assert_connected(self);
        redisAppendCommand(self->context, cmd);

SV *
redis_hiredis_get_reply(self)
    Redis::hiredis self
    PREINIT:
        redisReply *reply;
    CODE:
        assert_connected(self);
        redisGetReply(self->context, (void **) &reply);
        RETVAL = _read_reply(self, reply);
        freeReplyObject(reply);
    OUTPUT:
        RETVAL

Redis::hiredis
redis_hiredis__new(clazz, utf8)
    char *clazz
    bool utf8
    PREINIT:
        Redis__hiredis self;
    CODE:
        self = calloc(1, sizeof(struct redhi_obj));
        self->utf8 = utf8;
        RETVAL = self;
    OUTPUT:
        RETVAL

void
redis_hiredis_DESTROY(self)
    Redis::hiredis self
    CODE:
        if ( self->context != NULL )
            redisFree(self->context);