/*
 * Simple redis interface
 *
 * (c) 2020 Steve Bennett <steveb@workware.net.au>
 *
 * See LICENSE for license details.
 */
#include <jim.h>
#include <jim-eventloop.h>
#include <unistd.h>
#include <hiredis.h>

/**
 * Recursively decode a redis reply as Tcl data structure.
 */
static Jim_Obj *jim_redis_get_result(Jim_Interp *interp, redisReply *reply)
{
    int i;
    switch (reply->type) {
        case REDIS_REPLY_INTEGER:
            return Jim_NewIntObj(interp, reply->integer);
        case REDIS_REPLY_STATUS:
        case REDIS_REPLY_ERROR:
        case REDIS_REPLY_STRING:
            return Jim_NewStringObj(interp, reply->str, reply->len);
            break;
        case REDIS_REPLY_ARRAY:
            {
                Jim_Obj *obj = Jim_NewListObj(interp, NULL, 0);
                for (i = 0; i < reply->elements; i++) {
                    Jim_ListAppendElement(interp, obj, jim_redis_get_result(interp, reply->element[i]));
                }
                return obj;
            }
        case REDIS_REPLY_NIL:
            return Jim_NewStringObj(interp, NULL, 0);
        default:
            return Jim_NewStringObj(interp, "badtype", -1);
    }
}

/**
 * $r readable ?script?
 * - set or clear a readable script
 * $r close
 * - close (delete) the handle
 * $r read
 * - synchronously read a SUBSCRIBE response (typically from within readable)
 * $r <redis-command> ...
 * - invoke the redis command and return the decoded result
 */
static int jim_redis_subcmd(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    int i;
    redisContext *c = Jim_CmdPrivData(interp);
    const char **args;
    size_t *arglens;
    int ret = JIM_OK;

    redisReply *reply;

    if (argc < 2) {
        Jim_WrongNumArgs(interp, 1, argv, "cmd ?args ...?");
        return JIM_ERR;
    }

    if (Jim_CompareStringImmediate(interp, argv[1], "readable")) {
        /* Remove any existing handler */
        Jim_DeleteFileHandler(interp, c->fd, JIM_EVENT_READABLE);
        if (argc > 2) {
            Jim_CreateScriptFileHandler(interp, c->fd, JIM_EVENT_READABLE, argv[2]);
        }
        return JIM_OK;
    }
    if (Jim_CompareStringImmediate(interp, argv[1], "close")) {
        return Jim_DeleteCommand(interp, argv[0]);
    }
    if (Jim_CompareStringImmediate(interp, argv[1], "read")) {
        if (redisGetReply(c, (void **)&reply) != REDIS_OK) {
            reply = NULL;
        }
    }
    else {
        int nargs = argc - 1;
        args = Jim_Alloc(sizeof(*args) * nargs);
        arglens = Jim_Alloc(sizeof(*arglens) * nargs);
        for (i = 0; i < nargs; i++) {
            args[i] = Jim_String(argv[i + 1]);
            arglens[i] = Jim_Length(argv[i + 1]);
        }
        reply = redisCommandArgv(c, nargs, args, arglens);
        Jim_Free(args);
        Jim_Free(arglens);
    }
    /* sometimes commands return NULL */
    if (reply) {
        Jim_SetResult(interp, jim_redis_get_result(interp, reply));
        if (reply->type == REDIS_REPLY_ERROR) {
            ret = JIM_ERR;
        }
        freeReplyObject(reply);
    }
    else if (c->err) {
        Jim_SetResultFormatted(interp, "%#s: %s", argv[1], c->errstr);
        ret = JIM_ERR;
    }
    return ret;
}

static void jim_redis_del_proc(Jim_Interp *interp, void *privData)
{
    redisContext *c = privData;
    JIM_NOTUSED(interp);
    Jim_DeleteFileHandler(interp, c->fd, JIM_EVENT_READABLE);
    redisFree(c);
}

/**
 * redis <socket-stream>
 *
 * Returns a handle that can be used to communicate with the redis
 * instance over the socket.
 * The original socket handle is closed.
 */
static int jim_redis_cmd(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    redisContext *c;
    char buf[60];
    Jim_Obj *objv[2];
    long fd;
    int ret;

    if (argc != 2) {
        Jim_WrongNumArgs(interp, 1, argv, "socket-stream");
        return JIM_ERR;
    }

    /* Invoke getfd to get the file descriptor */
    objv[0] = argv[1];
    objv[1] = Jim_NewStringObj(interp, "getfd", -1);
    ret = Jim_EvalObjVector(interp, 2, objv);
    if (ret == JIM_OK) {
        ret = Jim_GetLong(interp, Jim_GetResult(interp), &fd) == JIM_ERR;
    }
    if (ret != JIM_OK) {
        Jim_SetResultFormatted(interp, "%#s: not a valid stream handle: %#s", argv[0], argv[1]);
        return ret;
    }

    /* Note that we dup the file descriptor here so that we can close the original */
    fd = dup(fd);
    /* Can't fail */
    c = redisConnectFd(fd);
    /* Now delete the original stream */
    Jim_DeleteCommand(interp, argv[1]);
    snprintf(buf, sizeof(buf), "redis.handle%ld", Jim_GetId(interp));
    Jim_CreateCommand(interp, buf, jim_redis_subcmd, c, jim_redis_del_proc);

    Jim_SetResult(interp, Jim_MakeGlobalNamespaceName(interp, Jim_NewStringObj(interp, buf, -1)));

    return JIM_OK;
}

int
Jim_redisInit(Jim_Interp *interp)
{
    Jim_PackageProvideCheck(interp, "redis");
    Jim_CreateCommand(interp, "redis", jim_redis_cmd, NULL, NULL);
    return JIM_OK;
}
