/* -*- Mode: C; tab-width: 4 -*- */
/*
  +----------------------------------------------------------------------+
  | Copyright (c) 1997-2009 The PHP Group                                |
  +----------------------------------------------------------------------+
  | This source file is subject to version 3.01 of the PHP license,      |
  | that is bundled with this package in the file LICENSE, and is        |
  | available through the world-wide-web at the following url:           |
  | http://www.php.net/license/3_01.txt                                  |
  | If you did not receive a copy of the PHP license and are unable to   |
  | obtain it through the world-wide-web, please send a note to          |
  | license@php.net so we can mail you a copy immediately.               |
  +----------------------------------------------------------------------+
  | Original Author: Michael Grunder <michael.grunder@gmail.com          |
  | Maintainer: Nicolas Favre-Felix <n.favre-felix@owlient.eu>           |
  +----------------------------------------------------------------------+
*/

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "redis_commands.h"

#include "php_network.h"

#ifndef PHP_WIN32
#include <netinet/tcp.h> /* TCP_KEEPALIVE */
#else
#include <winsock.h>
#endif

#ifdef HAVE_REDIS_JSON
#include <ext/json/php_json.h>
#endif

#include <zend_exceptions.h>

/* Georadius sort type */
typedef enum geoSortType {
    SORT_NONE,
    SORT_ASC,
    SORT_DESC
} geoSortType;

/* Georadius store type */
typedef enum geoStoreType {
    STORE_NONE,
    STORE_COORD,
    STORE_DIST
} geoStoreType;

/* Georadius options structure */
typedef struct geoOptions {
    int withcoord;
    int withdist;
    int withhash;
    long count;
    zend_bool any;
    geoSortType sort;
    geoStoreType store;
    zend_string *key;
} geoOptions;

typedef struct redisLcsOptions {
    zend_bool len;
    zend_bool idx;
    zend_long minmatchlen;
    zend_bool withmatchlen;
} redisLcsOptions;

typedef struct redisRestoreOptions {
    zend_bool replace;
    zend_bool absttl;
    zend_long idletime;
    zend_long freq;
} redisRestoreOptions;

#define REDIS_ZCMD_HAS_DST_KEY      (1 << 0)
#define REDIS_ZCMD_HAS_WITHSCORES   (1 << 1)
#define REDIS_ZCMD_HAS_BY_LEX_SCORE (1 << 2)
#define REDIS_ZCMD_HAS_REV          (1 << 3)
#define REDIS_ZCMD_HAS_LIMIT        (1 << 4)
#define REDIS_ZCMD_INT_RANGE        (1 << 5)
#define REDIS_ZCMD_HAS_AGGREGATE    (1 << 6)

/* ZRANGE, ZRANGEBYSCORE, ZRANGESTORE options */
typedef struct redisZcmdOptions {
    zend_bool withscores;
    zend_bool byscore;
    zend_bool bylex;
    zend_bool rev;
    zend_string *aggregate;
    struct {
        zend_bool enabled;
        zend_long offset;
        zend_long count;
    } limit;
} redisZcmdOptions;

/* Local passthrough macro for command construction.  Given that these methods
 * are generic (so they work whether the caller is Redis or RedisCluster) we
 * will always have redis_sock, slot*, and */
#define REDIS_CMD_SPPRINTF(ret, kw, fmt, ...) \
    redis_spprintf(redis_sock, slot, ret, kw, fmt, ##__VA_ARGS__)

/* Generic commands based on method signature and what kind of things we're
 * processing.  Lots of Redis commands take something like key, value, or
 * key, value long.  Each unique signature like this is written only once */

/* A command that takes no arguments */
int redis_empty_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                    char *kw, char **cmd, int *cmd_len, short *slot,
                    void **ctx)
{
    *cmd_len = REDIS_CMD_SPPRINTF(cmd, kw, "");
    return SUCCESS;
}

/* Helper to construct a raw command.  Given that the cluster and non cluster
 * versions are different (RedisCluster needs an additional argument to direct
 * the command) we take the start of our array and count */
int redis_build_raw_cmd(zval *z_args, int argc, char **cmd, int *cmd_len)
{
    smart_string cmdstr = {0};
    int i;

    /* Make sure our first argument is a string */
    if (Z_TYPE(z_args[0]) != IS_STRING) {
        php_error_docref(NULL, E_WARNING,
            "When sending a 'raw' command, the first argument must be a string!");
        return FAILURE;
    }

    /* Initialize our command string */
    redis_cmd_init_sstr(&cmdstr, argc-1, Z_STRVAL(z_args[0]), Z_STRLEN(z_args[0]));

    for (i = 1; i < argc; i++) {
       switch (Z_TYPE(z_args[i])) {
            case IS_STRING:
                redis_cmd_append_sstr(&cmdstr, Z_STRVAL(z_args[i]),
                    Z_STRLEN(z_args[i]));
                break;
            case IS_LONG:
                redis_cmd_append_sstr_long(&cmdstr,Z_LVAL(z_args[i]));
                break;
            case IS_DOUBLE:
                redis_cmd_append_sstr_dbl(&cmdstr,Z_DVAL(z_args[i]));
                break;
            default:
                php_error_docref(NULL, E_WARNING,
                    "Raw command arguments must be scalar values!");
                efree(cmdstr.c);
                return FAILURE;
        }
    }

    /* Push command and length to caller */
    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

smart_string *
redis_build_script_cmd(smart_string *cmd, int argc, zval *z_args)
{
    int i;
    zend_string *zstr;

    if (Z_TYPE(z_args[0]) != IS_STRING) {
        return NULL;
    }
    // Branch based on the directive
    if (zend_string_equals_literal_ci(Z_STR(z_args[0]), "kill")) {
        // Simple SCRIPT_KILL command
        REDIS_CMD_INIT_SSTR_STATIC(cmd, argc, "SCRIPT");
        redis_cmd_append_sstr(cmd, ZEND_STRL("KILL"));
    } else if (zend_string_equals_literal_ci(Z_STR(z_args[0]), "flush")) {
        // Simple SCRIPT FLUSH [ASYNC | SYNC]
        if (argc > 1 && (
            Z_TYPE(z_args[1]) != IS_STRING || (
                !zend_string_equals_literal_ci(Z_STR(z_args[1]), "sync") &&
                !zend_string_equals_literal_ci(Z_STR(z_args[1]), "async")
            )
        )) {
            return NULL;
        }
        REDIS_CMD_INIT_SSTR_STATIC(cmd, argc, "SCRIPT");
        redis_cmd_append_sstr(cmd, ZEND_STRL("FLUSH"));
        if (argc > 1) {
            redis_cmd_append_sstr(cmd, Z_STRVAL(z_args[1]), Z_STRLEN(z_args[1]));
        }
    } else if (zend_string_equals_literal_ci(Z_STR(z_args[0]), "load")) {
        // Make sure we have a second argument, and it's not empty.  If it is
        // empty, we can just return an empty array (which is what Redis does)
        if (argc < 2 || Z_TYPE(z_args[1]) != IS_STRING || Z_STRLEN(z_args[1]) < 1) {
            return NULL;
        }
        // Format our SCRIPT LOAD command
        REDIS_CMD_INIT_SSTR_STATIC(cmd, argc, "SCRIPT");
        redis_cmd_append_sstr(cmd, ZEND_STRL("LOAD"));
        redis_cmd_append_sstr(cmd, Z_STRVAL(z_args[1]), Z_STRLEN(z_args[1]));
    } else if (zend_string_equals_literal_ci(Z_STR(z_args[0]), "exists")) {
        // Make sure we have a second argument
        if (argc < 2) {
            return NULL;
        }
        /* Construct our SCRIPT EXISTS command */
        REDIS_CMD_INIT_SSTR_STATIC(cmd, argc, "SCRIPT");
        redis_cmd_append_sstr(cmd, ZEND_STRL("EXISTS"));

        for (i = 1; i < argc; ++i) {
            zstr = zval_get_string(&z_args[i]);
            redis_cmd_append_sstr(cmd, ZSTR_VAL(zstr), ZSTR_LEN(zstr));
            zend_string_release(zstr);
        }
    } else {
        /* Unknown directive */
        return NULL;
    }
    return cmd;
}

/* Command that takes one optional string */
int redis_opt_str_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, char *kw,
                      char **cmd, int *cmd_len, short *slot, void **ctx)
{
    char *arg = NULL;
    size_t arglen;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "|s!", &arg, &arglen) == FAILURE) {
        return FAILURE;
    }

    if (arg != NULL) {
        *cmd_len = REDIS_CMD_SPPRINTF(cmd, kw, "s", arg, arglen);
    } else {
        *cmd_len = REDIS_CMD_SPPRINTF(cmd, kw, "");
    }

    return SUCCESS;
}

/* Generic command where we just take a string and do nothing to it*/
int redis_str_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, char *kw,
                  char **cmd, int *cmd_len, short *slot, void **ctx)
{
    char *arg;
    size_t arg_len;

    // Parse args
    if (zend_parse_parameters(ZEND_NUM_ARGS(), "s", &arg, &arg_len)
                             ==FAILURE)
    {
        return FAILURE;
    }

    // Build the command without molesting the string
    *cmd_len = REDIS_CMD_SPPRINTF(cmd, kw, "s", arg, arg_len);

    return SUCCESS;
}

/* Key, long, zval (serialized) */
int redis_key_long_val_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                           char *kw, char **cmd, int *cmd_len, short *slot,
                           void **ctx)
{
    char *key = NULL;
    size_t key_len;
    zend_long expire;
    zval *z_val;

    ZEND_PARSE_PARAMETERS_START(3, 3)
        Z_PARAM_STRING(key, key_len)
        Z_PARAM_LONG(expire)
        Z_PARAM_ZVAL(z_val)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    *cmd_len = REDIS_CMD_SPPRINTF(cmd, kw, "klv", key, key_len, expire, z_val);

    return SUCCESS;
}

/* Generic key, long, string (unserialized) */
int redis_key_long_str_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                           char *kw, char **cmd, int *cmd_len, short *slot,
                           void **ctx)
{
    char *key, *val;
    size_t key_len, val_len;
    zend_long lval;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "sls", &key, &key_len,
                             &lval, &val, &val_len) == FAILURE)
    {
        return FAILURE;
    }

    *cmd_len = REDIS_CMD_SPPRINTF(cmd, kw, "kds", key, key_len, (int)lval, val, val_len);

    return SUCCESS;
}

/* Generic command construction when we just take a key and value */
int redis_kv_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                 char *kw, char **cmd, int *cmd_len, short *slot,
                 void **ctx)
{
    char *key;
    size_t key_len;
    zval *z_val;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "sz", &key, &key_len,
                             &z_val) == FAILURE)
    {
        return FAILURE;
    }

    *cmd_len = REDIS_CMD_SPPRINTF(cmd, kw, "kv", key, key_len, z_val);

    return SUCCESS;
}

/* Generic command that takes a key and an unserialized value */
int redis_key_str_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                      char *kw, char **cmd, int *cmd_len, short *slot,
                      void **ctx)
{
    char *key, *val;
    size_t key_len, val_len;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "ss", &key, &key_len,
                             &val, &val_len) == FAILURE)
    {
        return FAILURE;
    }

    // Construct command
    *cmd_len = REDIS_CMD_SPPRINTF(cmd, kw, "ks", key, key_len, val, val_len);

    return SUCCESS;
}

/* Key, string, string without serialization (ZCOUNT, ZREMRANGEBYSCORE) */
int redis_key_str_str_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                          char *kw, char **cmd, int *cmd_len, short *slot,
                          void **ctx)
{
    char *k, *v1, *v2;
    size_t klen, v1len, v2len;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "sss", &k, &klen,
                             &v1, &v1len, &v2, &v2len) == FAILURE)
    {
        return FAILURE;
    }

    *cmd_len = REDIS_CMD_SPPRINTF(cmd, kw, "kss", k, klen, v1, v1len, v2, v2len);

    // Success!
    return SUCCESS;
}

/* Generic command that takes two keys */
int redis_key_key_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                      char *kw, char **cmd, int *cmd_len, short *slot,
                      void **ctx)
{
    zend_string *key1 = NULL, *key2 = NULL;
    smart_string cmdstr = {0};
    short slot2;

    ZEND_PARSE_PARAMETERS_START(2, 2)
        Z_PARAM_STR(key1)
        Z_PARAM_STR(key2)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    redis_cmd_init_sstr(&cmdstr, 2, kw, strlen(kw));
    redis_cmd_append_sstr_key_zstr(&cmdstr, key1, redis_sock, slot);
    redis_cmd_append_sstr_key_zstr(&cmdstr, key2, redis_sock, slot ? &slot2 : NULL);

    if (slot && *slot != slot2) {
        php_error_docref(0, E_WARNING, "Keys don't hash to the same slot");
        smart_string_free(&cmdstr);
        return FAILURE;
    }

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;
    return SUCCESS;
}

/* Generic command construction where we take a key and a long */
int redis_key_long_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                       char *kw, char **cmd, int *cmd_len, short *slot,
                       void **ctx)
{
    zend_string *key = NULL;
    zend_long lval = 0;

    ZEND_PARSE_PARAMETERS_START(2, 2)
        Z_PARAM_STR(key)
        Z_PARAM_LONG(lval)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    *cmd_len = REDIS_CMD_SPPRINTF(cmd, kw, "kl", ZSTR_VAL(key), ZSTR_LEN(key), lval);

    return SUCCESS;
}

/* long, long */
int redis_long_long_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                        char *kw, char **cmd, int *cmd_len, short *slot,
                        void **ctx)
{
    zend_long l1 = 0, l2 = 0;

    ZEND_PARSE_PARAMETERS_START(2, 2)
        Z_PARAM_LONG(l1)
        Z_PARAM_LONG(l2)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    *cmd_len = REDIS_CMD_SPPRINTF(cmd, kw, "ll", l1, l2);

    return SUCCESS;
}

/* key, long, long */
int redis_key_long_long_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                            char *kw, char **cmd, int *cmd_len, short *slot,
                            void **ctx)
{
    char *key;
    size_t key_len;
    zend_long val1, val2;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "sll", &key, &key_len,
                             &val1, &val2) == FAILURE)
    {
        return FAILURE;
    }

    *cmd_len = REDIS_CMD_SPPRINTF(cmd, kw, "kll", key, key_len, val1, val2);

    return SUCCESS;
}

/* Generic command where we take a single key */
int redis_key_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                  char *kw, char **cmd, int *cmd_len, short *slot,
                  void **ctx)
{
    char *key;
    size_t key_len;

    ZEND_PARSE_PARAMETERS_START(1, 1)
        Z_PARAM_STRING(key, key_len);
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    *cmd_len = REDIS_CMD_SPPRINTF(cmd, kw, "k", key, key_len);

    return SUCCESS;
}

int
redis_failover_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                   char **cmd, int *cmd_len, short *slot, void **ctx)
{
    int argc;
    smart_string cmdstr = {0};
    zend_bool abort = 0, force = 0;
    zend_long timeout = 0, port = 0;
    zend_string *zkey, *host = NULL;
    zval *z_to = NULL, *z_ele;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "|a!bl",
                              &z_to, &abort, &timeout) == FAILURE)
    {
        return FAILURE;
    }

    if (z_to != NULL) {
        ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(z_to), zkey, z_ele) {
            if (zkey != NULL) {
                ZVAL_DEREF(z_ele);
                if (zend_string_equals_literal_ci(zkey, "host")) {
                    host = zval_get_string(z_ele);
                } else if (zend_string_equals_literal_ci(zkey, "port")) {
                    port = zval_get_long(z_ele);
                } else if (zend_string_equals_literal_ci(zkey, "force")) {
                    force = zval_is_true(z_ele);
                }
            }
        } ZEND_HASH_FOREACH_END();
        if (!host || !port) {
            php_error_docref(NULL, E_WARNING, "host and port must be provided!");
            if (host) zend_string_release(host);
            return FAILURE;
        }
    }

    argc = (host && port ? 3 + force : 0) + abort + (timeout > 0 ? 2 : 0);
    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, argc, "FAILOVER");

    if (host && port) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "TO");
        redis_cmd_append_sstr_zstr(&cmdstr, host);
        redis_cmd_append_sstr_int(&cmdstr, port);
        if (force) {
            REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "FORCE");
        }
        zend_string_release(host);
    }

    if (abort) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "ABORT");
    }
    if (timeout > 0) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "TIMEOUT");
        redis_cmd_append_sstr_long(&cmdstr, timeout);
    }

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

int redis_flush_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
               char *kw, char **cmd, int *cmd_len, short *slot, void **ctx)
{
    smart_string cmdstr = {0};
    zend_bool sync = 0;
    zend_bool is_null = 1;

    ZEND_PARSE_PARAMETERS_START(0, 1)
        Z_PARAM_OPTIONAL
        Z_PARAM_BOOL_OR_NULL(sync, is_null)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    redis_cmd_init_sstr(&cmdstr, !is_null, kw, strlen(kw));
    if (!is_null) {
        ZEND_ASSERT(sync == 0 || sync == 1);
        if (sync == 0) {
            REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "ASYNC");
        } else {
            REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "SYNC");
        }
    }

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

/* Generic command where we take a key and a double */
int redis_key_dbl_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                      char *kw, char **cmd, int *cmd_len, short *slot,
                      void **ctx)
{
    char *key;
    size_t key_len;
    double val;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "sd", &key, &key_len,
                             &val) == FAILURE)
    {
        return FAILURE;
    }

    *cmd_len = REDIS_CMD_SPPRINTF(cmd, kw, "kf", key, key_len, val);

    return SUCCESS;
}

/* Generic to construct SCAN and variant commands */
int redis_fmt_scan_cmd(char **cmd, REDIS_SCAN_TYPE type, char *key, int key_len,
                       uint64_t it, char *pat, int pat_len, long count)
{
    static char *kw[] = {"SCAN","SSCAN","HSCAN","ZSCAN"};
    int argc;
    smart_string cmdstr = {0};

    // Figure out our argument count
    argc = 1 + (type!=TYPE_SCAN) + (pat_len>0?2:0) + (count>0?2:0);

    redis_cmd_init_sstr(&cmdstr, argc, kw[type], strlen(kw[type]));

    // Append our key if it's not a regular SCAN command
    if (type != TYPE_SCAN) {
        redis_cmd_append_sstr(&cmdstr, key, key_len);
    }

    // Append cursor
    redis_cmd_append_sstr_u64(&cmdstr, it);

    // Append count if we've got one
    if (count) {
        redis_cmd_append_sstr(&cmdstr, ZEND_STRL("COUNT"));
        redis_cmd_append_sstr_long(&cmdstr, count);
    }

    // Append pattern if we've got one
    if (pat_len) {
        redis_cmd_append_sstr(&cmdstr, ZEND_STRL("MATCH"));
        redis_cmd_append_sstr(&cmdstr,pat,pat_len);
    }

    // Push command to the caller, return length
    *cmd = cmdstr.c;
    return cmdstr.len;
}

void redis_get_zcmd_options(redisZcmdOptions *dst, zval *src, int flags) {
    zval *zv, *zoff, *zcnt;
    zend_string *key;

    ZEND_ASSERT(dst != NULL);

    memset(dst, 0, sizeof(*dst));

    if (src == NULL)
        return;

    if (Z_TYPE_P(src) != IS_ARRAY) {
        if (Z_TYPE_P(src) == IS_TRUE && (flags & REDIS_ZCMD_HAS_WITHSCORES))
            dst->withscores = 1;
        return;
    }

    ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(src), key, zv) {
        ZVAL_DEREF(zv);

        if (key) {
            if ((flags & REDIS_ZCMD_HAS_WITHSCORES) && zend_string_equals_literal_ci(key, "WITHSCORES"))
                dst->withscores = zval_is_true(zv);
            else if ((flags & REDIS_ZCMD_HAS_LIMIT) && zend_string_equals_literal_ci(key, "LIMIT") &&
                     Z_TYPE_P(zv) == IS_ARRAY)
            {
                if ((zoff = zend_hash_index_find(Z_ARRVAL_P(zv), 0)) != NULL &&
                    (zcnt = zend_hash_index_find(Z_ARRVAL_P(zv), 1)) != NULL)
                {
                    dst->limit.enabled = 1;
                    dst->limit.offset = zval_get_long(zoff);
                    dst->limit.count = zval_get_long(zcnt);
                } else {
                    php_error_docref(NULL, E_WARNING, "LIMIT offset and count must be an array with twe elements");
                }
            } else if ((flags & REDIS_ZCMD_HAS_AGGREGATE && zend_string_equals_literal_ci(key, "AGGREGATE")) &&
                       Z_TYPE_P(zv) == IS_STRING)
            {
                if (Z_TYPE_P(zv) != IS_STRING || (!zend_string_equals_literal_ci(Z_STR_P(zv), "SUM") &&
                                                  !zend_string_equals_literal_ci(Z_STR_P(zv), "MIN") &&
                                                  !zend_string_equals_literal_ci(Z_STR_P(zv), "MAX")))
                {
                    php_error_docref(NULL, E_WARNING, "Valid AGGREGATE options are 'SUM', 'MIN', or 'MAX'");
                } else {
                    dst->aggregate = Z_STR_P(zv);
                }
            }
        } else if (Z_TYPE_P(zv) == IS_STRING) {
            key = Z_STR_P(zv);

            if ((flags & REDIS_ZCMD_HAS_BY_LEX_SCORE) && zend_string_equals_literal_ci(key, "BYSCORE"))
                dst->byscore = 1, dst->bylex = 0;
            else if ((flags & REDIS_ZCMD_HAS_BY_LEX_SCORE) && zend_string_equals_literal_ci(key, "BYLEX"))
                dst->bylex = 1, dst->byscore = 0;
            else if ((flags & REDIS_ZCMD_HAS_REV) && zend_string_equals_literal_ci(key, "REV"))
                dst->rev = 1;
            else if ((flags & REDIS_ZCMD_HAS_WITHSCORES && zend_string_equals_literal_ci(key, "WITHSCORES")))
                dst->withscores = 1;
        }
    } ZEND_HASH_FOREACH_END();
}

#if defined(__clang__) || \
    (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 3)))
#define PHPREDIS_HAVE_BSWAP32 1
#endif

static void redis_copy_fp32(char *dst, float src) {
#ifdef PHPREDIS_BIG_ENDIAN
#ifdef PHPREDIS_HAVE_BSWAP32
    uint32_t val;
    memcpy(&val, &src, sizeof(val));
    val = __builtin_bswap32(val);
    memcpy(dst, &val, sizeof(val));
#else
    union {
        float f;
        unsigned char b[4];
    } u;
    u.f = src;
    dst[0] = u.b[3];
    dst[1] = u.b[2];
    dst[2] = u.b[1];
    dst[3] = u.b[0];
#endif
#else
    memcpy(dst, &src, sizeof(src));
#endif
}

// + ZRANGE               key start stop [BYSCORE | BYLEX] [REV] [LIMIT offset count] [WITHSCORES]
// + ZRANGESTORE      dst src   min  max [BYSCORE | BYLEX] [REV] [LIMIT offset count]
// + ZREVRANGE            key start stop                                              [WITHSCORES]
// + ZRANGEBYSCORE        key   min  max                         [LIMIT offset count] [WITHSCORES]
// + ZREVRANGEBYSCORE     key   max  min                         [LIMIT offset count] [WITHSCORES]
// - ZRANGEBYLEX          key   min  max                         [LIMIT offset count]
// - ZREVRANGEBYLEX       key   max  min                         [LIMIT offset count]
// - ZDIFF                                                                            [WITHSCORES]
// - ZUNION                                                                           [WITHSCORES] [AGGREGATE X]
// - ZINTER                                                                           [WITHSCORES] [AGGREGATE X]
static int redis_get_zcmd_flags(const char *kw) {
    size_t len = strlen(kw);

    if (REDIS_STRICMP_STATIC(kw, len, "ZRANGESTORE")) {
        return REDIS_ZCMD_HAS_DST_KEY |
               REDIS_ZCMD_HAS_WITHSCORES |
               REDIS_ZCMD_HAS_BY_LEX_SCORE |
               REDIS_ZCMD_HAS_REV |
               REDIS_ZCMD_HAS_LIMIT;
    } else if (REDIS_STRICMP_STATIC(kw, len, "ZRANGE")) {
        return REDIS_ZCMD_HAS_WITHSCORES |
               REDIS_ZCMD_HAS_BY_LEX_SCORE |
               REDIS_ZCMD_HAS_REV |
               REDIS_ZCMD_HAS_LIMIT;
    } else if (REDIS_STRICMP_STATIC(kw, len, "ZREVRANGE")) {
        return REDIS_ZCMD_HAS_WITHSCORES |
               REDIS_ZCMD_INT_RANGE;
    } else if (REDIS_STRICMP_STATIC(kw, len, "ZRANGEBYSCORE") ||
               REDIS_STRICMP_STATIC(kw, len, "ZREVRANGEBYSCORE"))
    {
        return REDIS_ZCMD_HAS_LIMIT |
               REDIS_ZCMD_HAS_WITHSCORES;
    } else if (REDIS_STRICMP_STATIC(kw, len, "ZRANGEBYLEX") ||
               REDIS_STRICMP_STATIC(kw, len, "ZREVRANGEBYLEX"))
    {
        return REDIS_ZCMD_HAS_LIMIT;
    } else if (REDIS_STRICMP_STATIC(kw, len, "ZDIFF")) {
        return REDIS_ZCMD_HAS_WITHSCORES;
    } else if (REDIS_STRICMP_STATIC(kw, len, "ZINTER") ||
               REDIS_STRICMP_STATIC(kw, len, "ZUNION"))
    {
        return REDIS_ZCMD_HAS_WITHSCORES |
               REDIS_ZCMD_HAS_AGGREGATE;
    }

    /* Reaching this line means a compile-time error */
    ZEND_ASSERT(0);
}

/* Validate ZLEX* min/max argument strings */
static int validate_zlex_arg(const char *str, size_t len) {
    return (len  > 1 && (*str == '[' || *str == '(')) ||
           (len == 1 && (*str == '+' || *str == '-'));
}

static int validate_zlex_arg_zval(zval *z) {
    return Z_TYPE_P(z) == IS_STRING && validate_zlex_arg(Z_STRVAL_P(z), Z_STRLEN_P(z));
}

int redis_zrange_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                     char *kw, char **cmd, int *cmd_len, short *slot,
                     void **ctx)
{
    zval *zoptions = NULL, *zstart = NULL, *zend = NULL;
    zend_string *dst = NULL, *src = NULL;
    zend_long start = 0, end = 0;
    smart_string cmdstr = {0};
    redisZcmdOptions opt;
    int min_argc, flags;
    short slot2;

    flags = redis_get_zcmd_flags(kw);

    min_argc = 3 + (flags & REDIS_ZCMD_HAS_DST_KEY);
    ZEND_PARSE_PARAMETERS_START(min_argc, min_argc + 1)
        if (flags & REDIS_ZCMD_HAS_DST_KEY) {
            Z_PARAM_STR(dst)
        }
        Z_PARAM_STR(src)
        if (flags & REDIS_ZCMD_INT_RANGE) {
            Z_PARAM_LONG(start)
            Z_PARAM_LONG(end)
        } else {
            Z_PARAM_ZVAL(zstart)
            Z_PARAM_ZVAL(zend)
        }
        Z_PARAM_OPTIONAL
        Z_PARAM_ZVAL_OR_NULL(zoptions)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    redis_get_zcmd_options(&opt, zoptions, flags);

    if (opt.bylex) {
        ZEND_ASSERT(!(flags & REDIS_ZCMD_INT_RANGE));
        if (!validate_zlex_arg_zval(zstart) || !validate_zlex_arg_zval(zend)) {
            php_error_docref(NULL, E_WARNING, "Legographical args must start with '[' or '(' or be '+' or '-'");
            return FAILURE;
        }
    }

    redis_cmd_init_sstr(&cmdstr, min_argc + !!opt.bylex + !!opt.byscore +
                                 !!opt.rev + !!opt.withscores +
                                 (opt.limit.enabled ? 3 : 0), kw, strlen(kw));

    if (flags & REDIS_ZCMD_HAS_DST_KEY)
        redis_cmd_append_sstr_key_zstr(&cmdstr, dst, redis_sock, slot);
    redis_cmd_append_sstr_key_zstr(&cmdstr, src, redis_sock, &slot2);

    /* Protect the user from crossslot errors */
    if ((flags & REDIS_ZCMD_HAS_DST_KEY) && slot && *slot != slot2) {
        php_error_docref(NULL, E_WARNING, "destination and source keys must map to the same slot");
        efree(cmdstr.c);
        return FAILURE;
    }

    if (flags & REDIS_ZCMD_INT_RANGE) {
        redis_cmd_append_sstr_long(&cmdstr, start);
        redis_cmd_append_sstr_long(&cmdstr, end);
    } else {
        redis_cmd_append_sstr_zval(&cmdstr, zstart, NULL);
        redis_cmd_append_sstr_zval(&cmdstr, zend, NULL);
    }

    REDIS_CMD_APPEND_SSTR_OPT_STATIC(&cmdstr, opt.byscore, "BYSCORE");
    REDIS_CMD_APPEND_SSTR_OPT_STATIC(&cmdstr, opt.bylex, "BYLEX");
    REDIS_CMD_APPEND_SSTR_OPT_STATIC(&cmdstr, opt.rev, "REV");

    if (opt.limit.enabled) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "LIMIT");
        redis_cmd_append_sstr_long(&cmdstr, opt.limit.offset);
        redis_cmd_append_sstr_long(&cmdstr, opt.limit.count);
    }

    REDIS_CMD_APPEND_SSTR_OPT_STATIC(&cmdstr, opt.withscores, "WITHSCORES");

    if (slot) *slot = slot2;
    *ctx = opt.withscores ? PHPREDIS_CTX_PTR : NULL;
    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

static int redis_build_config_get_cmd(smart_string *dst, zval *val) {
    zend_string *zstr;
    int ncfg;
    zval *zv;

    if (val == NULL || (Z_TYPE_P(val) != IS_STRING && Z_TYPE_P(val) != IS_ARRAY)) {
        php_error_docref(NULL, E_WARNING, "Must pass a string or array of values to CONFIG GET");
        return FAILURE;
    } else if (Z_TYPE_P(val) == IS_ARRAY && zend_hash_num_elements(Z_ARRVAL_P(val)) == 0) {
        php_error_docref(NULL, E_WARNING, "Cannot pass an empty array to CONFIG GET");
        return FAILURE;
    }

    ncfg = Z_TYPE_P(val) == IS_STRING ? 1 : zend_hash_num_elements(Z_ARRVAL_P(val));

    REDIS_CMD_INIT_SSTR_STATIC(dst, 1 + ncfg, "CONFIG");
    REDIS_CMD_APPEND_SSTR_STATIC(dst, "GET");

    if (Z_TYPE_P(val) == IS_STRING) {
        redis_cmd_append_sstr_zstr(dst, Z_STR_P(val));
    } else {
        ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(val), zv) {
            ZVAL_DEREF(zv);

            zstr = zval_get_string(zv);
            redis_cmd_append_sstr_zstr(dst, zstr);
            zend_string_release(zstr);
        } ZEND_HASH_FOREACH_END();
    }

    return SUCCESS;
}

static int redis_build_config_set_cmd(smart_string *dst, zval *key, zend_string *val) {
    zend_string *zkey, *zstr;
    zval *zv;

    /* Legacy case:  CONFIG SET <string> <string> */
    if (key != NULL && val != NULL) {
        REDIS_CMD_INIT_SSTR_STATIC(dst, 3, "CONFIG");
        REDIS_CMD_APPEND_SSTR_STATIC(dst, "SET");

        zstr = zval_get_string(key);
        redis_cmd_append_sstr_zstr(dst, zstr);
        zend_string_release(zstr);

        redis_cmd_append_sstr_zstr(dst, val);

        return SUCCESS;
    }

    /* Now we must have an array with at least one element */
    if (key == NULL || Z_TYPE_P(key) != IS_ARRAY || zend_hash_num_elements(Z_ARRVAL_P(key)) == 0) {
        php_error_docref(NULL, E_WARNING, "Must either pass two strings to CONFIG SET or a non-empty array of values");
        return FAILURE;
    }

    REDIS_CMD_INIT_SSTR_STATIC(dst, 1 + (2 * zend_hash_num_elements(Z_ARRVAL_P(key))), "CONFIG");
    REDIS_CMD_APPEND_SSTR_STATIC(dst, "SET");

    ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(key), zkey, zv) {
        if (zkey == NULL)
            goto fail;

        ZVAL_DEREF(zv);

        redis_cmd_append_sstr_zstr(dst, zkey);

        zstr = zval_get_string(zv);
        redis_cmd_append_sstr_zstr(dst, zstr);
        zend_string_release(zstr);
    } ZEND_HASH_FOREACH_END();

    return SUCCESS;

fail:
    php_error_docref(NULL, E_WARNING, "Must pass an associate array of config keys and values");
    efree(dst->c);
    memset(dst, 0, sizeof(*dst));
    return FAILURE;
}

int
redis_config_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                 char **cmd, int *cmd_len, short *slot, void **ctx)
{
    zend_string *op = NULL, *arg = NULL;
    smart_string cmdstr = {0};
    int res = FAILURE;
    zval *key = NULL;

    ZEND_PARSE_PARAMETERS_START(1, 3)
        Z_PARAM_STR(op)
        Z_PARAM_OPTIONAL
        Z_PARAM_ZVAL_OR_NULL(key)
        Z_PARAM_STR_OR_NULL(arg)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    if (zend_string_equals_literal_ci(op, "RESETSTAT") ||
        zend_string_equals_literal_ci(op, "REWRITE"))
    {
        REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 1, "CONFIG");
        redis_cmd_append_sstr_zstr(&cmdstr, op);
        *ctx = redis_boolean_response;
        res  = SUCCESS;
    } else if (zend_string_equals_literal_ci(op, "GET")) {
        res  = redis_build_config_get_cmd(&cmdstr, key);
        *ctx = redis_mbulk_reply_zipped_raw;
    } else if (zend_string_equals_literal_ci(op, "SET")) {
        res  = redis_build_config_set_cmd(&cmdstr, key, arg);
        *ctx = redis_boolean_response;
    } else {
        php_error_docref(NULL, E_WARNING, "Unknown operation '%s'", ZSTR_VAL(op));
        return FAILURE;
    }

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;
    return res;
}

int
redis_function_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                   char **cmd, int *cmd_len, short *slot, void **ctx)
{
    smart_string cmdstr = {0};
    zend_string *op = NULL, *arg;
    zval *argv = NULL;
    int i, argc = 0;

    ZEND_PARSE_PARAMETERS_START(1, -1)
        Z_PARAM_STR(op)
        Z_PARAM_OPTIONAL
        Z_PARAM_VARIADIC('*', argv, argc)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    for (i = 0; i < argc; ++i) {
        if (Z_TYPE(argv[i]) != IS_STRING) {
            php_error_docref(NULL, E_WARNING, "invalid argument");
            return FAILURE;
        }
    }

    if (zend_string_equals_literal_ci(op, "DELETE")) {
        if (argc < 1) {
            php_error_docref(NULL, E_WARNING, "argument required");
            return FAILURE;
        }
    } else if (zend_string_equals_literal_ci(op, "DUMP")) {
        *ctx = PHPREDIS_CTX_PTR;
    } else if (zend_string_equals_literal_ci(op, "FLUSH")) {
        if (argc > 0 &&
            !zend_string_equals_literal_ci(Z_STR(argv[0]), "SYNC") &&
            !zend_string_equals_literal_ci(Z_STR(argv[0]), "ASYNC")
        ) {
            php_error_docref(NULL, E_WARNING, "invalid argument");
            return FAILURE;
        }
    } else if (zend_string_equals_literal_ci(op, "KILL")) {
        // noop
    } else if (zend_string_equals_literal_ci(op, "LIST")) {
        if (argc > 0) {
            if (zend_string_equals_literal_ci(Z_STR(argv[0]), "LIBRARYNAME")) {
                if (argc < 2) {
                    php_error_docref(NULL, E_WARNING, "argument required");
                    return FAILURE;
                }
            } else if (!zend_string_equals_literal_ci(Z_STR(argv[0]), "WITHCODE")) {
                php_error_docref(NULL, E_WARNING, "invalid argument");
                return FAILURE;
            }
        }
        *ctx = PHPREDIS_CTX_PTR + 1;
    } else if (zend_string_equals_literal_ci(op, "LOAD")) {
        if (argc < 1 || (
            zend_string_equals_literal_ci(Z_STR(argv[0]), "REPLACE") && argc < 2
        )) {
            php_error_docref(NULL, E_WARNING, "argument required");
            return FAILURE;
        }
        *ctx = PHPREDIS_CTX_PTR;
    } else if (zend_string_equals_literal_ci(op, "RESTORE")) {
        if (argc < 1 || (
            argc > 1 &&
            !zend_string_equals_literal_ci(Z_STR(argv[1]), "FLUSH") &&
            !zend_string_equals_literal_ci(Z_STR(argv[1]), "APPEND") &&
            !zend_string_equals_literal_ci(Z_STR(argv[1]), "REPLACE")
        )) {
            php_error_docref(NULL, E_WARNING, "invalid argument");
            return FAILURE;
        }
    } else if (zend_string_equals_literal_ci(op, "STATS")) {
        *ctx = PHPREDIS_CTX_PTR + 1;
    } else {
        php_error_docref(NULL, E_WARNING, "Unknown operation '%s'", ZSTR_VAL(op));
        return FAILURE;
    }

    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 1 + argc, "FUNCTION");
    redis_cmd_append_sstr_zstr(&cmdstr, op);

    for (i = 0; i < argc; i++) {
        arg = zval_get_string(&argv[i]);
        redis_cmd_append_sstr_zstr(&cmdstr, arg);
        zend_string_release(arg);
    }

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

int
redis_fcall_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                char *kw, char **cmd, int *cmd_len, short *slot, void **ctx)
{
    HashTable *keys = NULL, *args = NULL;
    smart_string cmdstr = {0};
    zend_string *fn = NULL;
    zval *zv;

    ZEND_PARSE_PARAMETERS_START(1, 3)
        Z_PARAM_STR(fn)
        Z_PARAM_OPTIONAL
        Z_PARAM_ARRAY_HT(keys)
        Z_PARAM_ARRAY_HT(args)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    redis_cmd_init_sstr(&cmdstr, 2 + (keys ? zend_hash_num_elements(keys) : 0) +
        (args ? zend_hash_num_elements(args) : 0), kw, strlen(kw));
    redis_cmd_append_sstr_zstr(&cmdstr, fn);
    redis_cmd_append_sstr_long(&cmdstr, keys ? zend_hash_num_elements(keys) : 0);

    if (keys != NULL) {
        ZEND_HASH_FOREACH_VAL(keys, zv) {
            redis_cmd_append_sstr_key_zval(&cmdstr, zv, redis_sock, slot);
        } ZEND_HASH_FOREACH_END();
    }

    if (args != NULL) {
        ZEND_HASH_FOREACH_VAL(args, zv) {
            redis_cmd_append_sstr_zval(&cmdstr, zv, redis_sock);
        } ZEND_HASH_FOREACH_END();
    }

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

int
redis_zrandmember_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                      char **cmd, int *cmd_len, short *slot, void **ctx)
{
    char *key;
    int count = 0;
    size_t key_len;
    smart_string cmdstr = {0};
    zend_bool withscores = 0;
    zval *z_opts = NULL, *z_ele;
    zend_string *zkey;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "s|a",
                              &key, &key_len, &z_opts) == FAILURE)
    {
        return FAILURE;
    }

    if (z_opts != NULL) {
        ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(z_opts), zkey, z_ele) {
            if (zkey != NULL) {
                ZVAL_DEREF(z_ele);
                if (zend_string_equals_literal_ci(zkey, "count")) {
                    count = zval_get_long(z_ele);
                } else if (zend_string_equals_literal_ci(zkey, "withscores")) {
                    withscores = zval_is_true(z_ele);
                }
            }
        } ZEND_HASH_FOREACH_END();
    }

    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 1 + (count != 0) + withscores, "ZRANDMEMBER");
    redis_cmd_append_sstr_key(&cmdstr, key, key_len, redis_sock, slot);

    if (count != 0) {
        redis_cmd_append_sstr_long(&cmdstr, count);
        *ctx = PHPREDIS_CTX_PTR;
    }

    if (withscores) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "WITHSCORES");
        *ctx = PHPREDIS_CTX_PTR + 1;
    }


    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;
    return SUCCESS;
}

int
redis_zdiff_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                char **cmd, int *cmd_len, short *slot, void **ctx)
{
    zval *z_keys, *z_opts = NULL, *z_key;
    redisZcmdOptions opts = {0};
    smart_string cmdstr = {0};
    int numkeys, flags;
    short s2 = 0;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "a|a",
                              &z_keys, &z_opts) == FAILURE)
    {
        return FAILURE;
    }

    if ((numkeys = zend_hash_num_elements(Z_ARRVAL_P(z_keys))) == 0) {
        return FAILURE;
    }

    flags = redis_get_zcmd_flags("ZDIFF");
    redis_get_zcmd_options(&opts, z_opts, flags);

    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 1 + numkeys + opts.withscores, "ZDIFF");
    redis_cmd_append_sstr_long(&cmdstr, numkeys);

    ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(z_keys), z_key) {
        ZVAL_DEREF(z_key);
        redis_cmd_append_sstr_key_zval(&cmdstr, z_key, redis_sock, slot);

        if (slot && s2 && s2 != *slot) {
            php_error_docref(NULL, E_WARNING, "Not all keys map to the same slot!");
            efree(cmdstr.c);
            return FAILURE;
        }

        if (slot) s2 = *slot;
    } ZEND_HASH_FOREACH_END();

    if (opts.withscores) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "WITHSCORES");
        *ctx = PHPREDIS_CTX_PTR;
    }

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;
    return SUCCESS;
}

static int redis_cmd_append_sstr_score(smart_string *dst, zval *score) {
    zend_uchar type;
    zend_long lval;
    size_t cmdlen;
    double dval;

    /* Get current command length */
    cmdlen = dst->len;

    if (Z_TYPE_P(score) == IS_LONG) {
        redis_cmd_append_sstr_zend_long(dst, Z_LVAL_P(score));
    } else if (Z_TYPE_P(score) == IS_DOUBLE) {
        redis_cmd_append_sstr_dbl(dst, Z_DVAL_P(score));
    } else if (Z_TYPE_P(score) == IS_STRING) {
        type = is_numeric_string(Z_STRVAL_P(score), Z_STRLEN_P(score), &lval, &dval, 0);
        if (type == IS_LONG) {
            redis_cmd_append_sstr_long(dst, lval);
        } else if (type == IS_DOUBLE) {
            redis_cmd_append_sstr_dbl(dst, dval);
        } else if (zend_string_equals_literal_ci(Z_STR_P(score), "-inf") ||
                   zend_string_equals_literal_ci(Z_STR_P(score), "+inf") ||
                   zend_string_equals_literal_ci(Z_STR_P(score), "inf"))
        {
            redis_cmd_append_sstr_zstr(dst, Z_STR_P(score));
        }
    }

    /* Success if we appended something */
    if (dst->len > cmdlen)
        return SUCCESS;

    /* Nothing appended, failure */
    php_error_docref(NULL, E_WARNING, "scores must be numeric or '-inf', 'inf', '+inf'");
    return FAILURE;
}

int redis_intercard_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                        char *kw, char **cmd, int *cmd_len, short *slot,
                        void **ctx)
{
    smart_string cmdstr = {0};
    zend_long limit = -1;
    HashTable *keys;
    zend_string *key;
    zval *zv;

    ZEND_PARSE_PARAMETERS_START(1, 2)
        Z_PARAM_ARRAY_HT(keys)
        Z_PARAM_OPTIONAL
        Z_PARAM_LONG(limit)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    if (zend_hash_num_elements(keys) == 0) {
        php_error_docref(NULL, E_WARNING, "Must pass at least one key");
        return FAILURE;
    } else if (ZEND_NUM_ARGS() == 2 && limit < 0) {
        php_error_docref(NULL, E_WARNING, "LIMIT cannot be negative");
        return FAILURE;
    }

    redis_cmd_init_sstr(&cmdstr, 1 + zend_hash_num_elements(keys) + (limit > 0 ? 2 : 0), kw, strlen(kw));
    redis_cmd_append_sstr_long(&cmdstr, zend_hash_num_elements(keys));

    if (slot) *slot = -1;

    ZEND_HASH_FOREACH_VAL(keys, zv) {
        key = redis_key_prefix_zval(redis_sock, zv);

        if (slot) {
            if (*slot == -1) {
                *slot = cluster_hash_key_zstr(key);
            } else if (*slot != cluster_hash_key_zstr(key)) {
                php_error_docref(NULL, E_WARNING, "All keys don't hash to the same slot");
                efree(cmdstr.c);
                zend_string_release(key);
                return FAILURE;
            }
        }

        redis_cmd_append_sstr_zstr(&cmdstr, key);
        zend_string_release(key);
    } ZEND_HASH_FOREACH_END();

    if (limit > 0) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "LIMIT");
        redis_cmd_append_sstr_long(&cmdstr, limit);
    }

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;
    return SUCCESS;
}

int redis_replicaof_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                        char *kw, char **cmd, int *cmd_len, short *slot,
                        void **ctx)
{
    smart_string cmdstr = {0};
    zend_string *host = NULL;
    zend_long port = 6379;

    ZEND_PARSE_PARAMETERS_START(0, 2)
        Z_PARAM_OPTIONAL
        Z_PARAM_STR(host)
        Z_PARAM_LONG(port)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    if (port < 0 || port > UINT16_MAX) {
        php_error_docref(NULL, E_WARNING, "Invalid port %ld", (long)port);
        return FAILURE;
    }

    redis_cmd_init_sstr(&cmdstr, 2, kw, strlen(kw));

    if (ZEND_NUM_ARGS() == 2) {
        redis_cmd_append_sstr_zstr(&cmdstr, host);
        redis_cmd_append_sstr_long(&cmdstr, port);
    } else {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "NO");
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "ONE");
    }

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

int
redis_zinterunion_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                      char *kw, char **cmd, int *cmd_len, short *slot,
                      void **ctx)
{
    zval *z_keys, *z_weights = NULL, *z_opts = NULL, *z_ele;
    redisZcmdOptions opts = {0};
    smart_string cmdstr = {0};
    int numkeys, flags;
    short s2 = 0;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "a|a!a",
                             &z_keys, &z_weights, &z_opts) == FAILURE)
    {
        return FAILURE;
    }

    if ((numkeys = zend_hash_num_elements(Z_ARRVAL_P(z_keys))) == 0) {
        return FAILURE;
    }

    if (z_weights && zend_hash_num_elements(Z_ARRVAL_P(z_weights)) != numkeys) {
        php_error_docref(NULL, E_WARNING, "WEIGHTS and keys array should be the same size!");
        return FAILURE;
    }

    flags = redis_get_zcmd_flags(kw);
    redis_get_zcmd_options(&opts, z_opts, flags);

    redis_cmd_init_sstr(&cmdstr, 1 + numkeys + (z_weights ? 1 + numkeys : 0) + (opts.aggregate ? 2 : 0) + opts.withscores, kw, strlen(kw));
    redis_cmd_append_sstr_long(&cmdstr, numkeys);

    ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(z_keys), z_ele) {
        ZVAL_DEREF(z_ele);
        redis_cmd_append_sstr_key_zval(&cmdstr, z_ele, redis_sock, slot);
        if (slot) {
            if (s2 && s2 != *slot) {
                php_error_docref(NULL, E_WARNING, "Not all keys hash to the same slot");
                efree(cmdstr.c);
                return FAILURE;
            }
            s2 = *slot;
        }
    } ZEND_HASH_FOREACH_END();

    if (z_weights) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "WEIGHTS");
        ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(z_weights), z_ele) {
            ZVAL_DEREF(z_ele);
            if (redis_cmd_append_sstr_score(&cmdstr, z_ele) == FAILURE) {
                efree(cmdstr.c);
                return FAILURE;
            }
        } ZEND_HASH_FOREACH_END();
    }

    if (opts.aggregate) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "AGGREGATE");
        redis_cmd_append_sstr_zstr(&cmdstr, opts.aggregate);
    }

    if (opts.withscores) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "WITHSCORES");
        *ctx = PHPREDIS_CTX_PTR;
    }

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;
    return SUCCESS;
}

int
redis_zdiffstore_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                     char **cmd, int *cmd_len, short *slot, void **ctx)
{
    smart_string cmdstr = {0};
    zend_string *dst = NULL;
    HashTable *keys = NULL;
    zend_ulong nkeys;
    short s2 = 0;
    zval *zkey;

    ZEND_PARSE_PARAMETERS_START(2, 2)
        Z_PARAM_STR(dst)
        Z_PARAM_ARRAY_HT(keys)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    nkeys = zend_hash_num_elements(keys);
    if (nkeys == 0)
        return FAILURE;

    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 2 + nkeys, "ZDIFFSTORE");
    redis_cmd_append_sstr_key_zstr(&cmdstr, dst, redis_sock, slot);
    redis_cmd_append_sstr_long(&cmdstr, nkeys);

    ZEND_HASH_FOREACH_VAL(keys, zkey) {
        ZVAL_DEREF(zkey);
        redis_cmd_append_sstr_key_zval(&cmdstr, zkey, redis_sock, slot ? &s2 : NULL);
        if (slot && *slot != s2) {
            php_error_docref(NULL, E_WARNING, "All keys must hash to the same slot");
            efree(cmdstr.c);
            return FAILURE;
        }
    } ZEND_HASH_FOREACH_END();

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;
    return SUCCESS;
}

/* ZUNIONSTORE, ZINTERSTORE */
int
redis_zinterunionstore_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                     char *kw, char **cmd, int *cmd_len, short *slot,
                     void **ctx)
{
    HashTable *keys = NULL, *weights = NULL;
    smart_string cmdstr = {0};
    zend_string *dst = NULL;
    zend_string *agg = NULL;
    zend_ulong nkeys;
    zval *zv = NULL;
    short s2 = 0;

    ZEND_PARSE_PARAMETERS_START(2, 4)
        Z_PARAM_STR(dst)
        Z_PARAM_ARRAY_HT(keys)
        Z_PARAM_OPTIONAL
        Z_PARAM_ARRAY_HT_OR_NULL(weights)
        Z_PARAM_STR_OR_NULL(agg)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    nkeys = zend_hash_num_elements(keys);
    if (nkeys == 0)
        return FAILURE;

    if (weights != NULL && zend_hash_num_elements(weights) != nkeys) {
        php_error_docref(NULL, E_WARNING, "WEIGHTS and keys array must be the same size!");
        return FAILURE;
    }

    // AGGREGATE option
    if (agg != NULL && (!zend_string_equals_literal_ci(agg, "SUM") &&
                        !zend_string_equals_literal_ci(agg, "MIN") &&
                        !zend_string_equals_literal_ci(agg, "MAX")))
    {
        php_error_docref(NULL, E_WARNING, "AGGREGATE option must be 'SUM', 'MIN', or 'MAX'");
        return FAILURE;
    }

    redis_cmd_init_sstr(&cmdstr, 2 + nkeys + (weights ? 1 + nkeys : 0) + (agg ? 2 : 0), kw, strlen(kw));
    redis_cmd_append_sstr_key_zstr(&cmdstr, dst, redis_sock, slot);
    redis_cmd_append_sstr_int(&cmdstr, nkeys);

    ZEND_HASH_FOREACH_VAL(keys, zv) {
        ZVAL_DEREF(zv);
        redis_cmd_append_sstr_key_zval(&cmdstr, zv, redis_sock, slot ? &s2 : NULL);
        if (slot && s2 != *slot) {
            php_error_docref(NULL, E_WARNING, "All keys don't hash to the same slot!");
            efree(cmdstr.c);
            return FAILURE;
        }
    } ZEND_HASH_FOREACH_END();

    if (weights) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "WEIGHTS");
        ZEND_HASH_FOREACH_VAL(weights, zv) {
            ZVAL_DEREF(zv);
            if (redis_cmd_append_sstr_score(&cmdstr, zv) == FAILURE) {
                efree(cmdstr.c);
                return FAILURE;
            }
        } ZEND_HASH_FOREACH_END();
    }

    if (agg) {
        redis_cmd_append_sstr(&cmdstr, ZEND_STRL("AGGREGATE"));
        redis_cmd_append_sstr_zstr(&cmdstr, agg);
    }

    // Push out values
    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

int redis_pubsub_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                     char **cmd, int *cmd_len, short *slot, void **ctx)
{
    HashTable *channels = NULL;
    smart_string cmdstr = {0};
    zend_string *op, *pattern = NULL;
    zval *arg = NULL, *z_chan;

    ZEND_PARSE_PARAMETERS_START(1, 2)
        Z_PARAM_STR(op)
        Z_PARAM_OPTIONAL
        Z_PARAM_ZVAL(arg)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    if (zend_string_equals_literal_ci(op, "NUMPAT")) {
        *ctx = NULL;
    } else if (zend_string_equals_literal_ci(op, "CHANNELS") ||
        zend_string_equals_literal_ci(op, "SHARDCHANNELS")
    ) {
        if (arg != NULL) {
            if (Z_TYPE_P(arg) != IS_STRING) {
                php_error_docref(NULL, E_WARNING, "Invalid pattern value");
                return FAILURE;
            }
            pattern = zval_get_string(arg);
        }
        *ctx = PHPREDIS_CTX_PTR;
    } else if (zend_string_equals_literal_ci(op, "NUMSUB") ||
        zend_string_equals_literal_ci(op, "SHARDNUMSUB")
    ) {
        if (arg != NULL) {
            if (Z_TYPE_P(arg) != IS_ARRAY) {
                php_error_docref(NULL, E_WARNING, "Invalid channels value");
                return FAILURE;
            }
            channels = Z_ARRVAL_P(arg);
        }
        *ctx = PHPREDIS_CTX_PTR + 1;
    } else {
        php_error_docref(NULL, E_WARNING, "Unknown PUBSUB operation '%s'", ZSTR_VAL(op));
        return FAILURE;
    }

    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 1 + !!pattern + (channels ? zend_hash_num_elements(channels) : 0), "PUBSUB");
    redis_cmd_append_sstr_zstr(&cmdstr, op);

    if (pattern != NULL) {
        redis_cmd_append_sstr_zstr(&cmdstr, pattern);
        zend_string_release(pattern);
    } else if (channels != NULL) {
        ZEND_HASH_FOREACH_VAL(channels, z_chan) {
            redis_cmd_append_sstr_key_zval(&cmdstr, z_chan, redis_sock, slot);
        } ZEND_HASH_FOREACH_END();
    }

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

/* SUBSCRIBE/PSUBSCRIBE/SSUBSCRIBE */
int redis_subscribe_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                        char *kw, char **cmd, int *cmd_len, short *slot,
                        void **ctx)
{
    zval *z_arr, *z_chan;
    HashTable *ht_chan;
    smart_string cmdstr = {0};
    subscribeContext *sctx = ecalloc(1, sizeof(*sctx));
    unsigned short shardslot = REDIS_CLUSTER_SLOTS;
    short s2;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "af", &z_arr,
                             &sctx->cb.fci, &sctx->cb.fci_cache) == FAILURE)
    {
        efree(sctx);
        return FAILURE;
    }

    ht_chan    = Z_ARRVAL_P(z_arr);
    sctx->kw   = kw;
    sctx->argc = zend_hash_num_elements(ht_chan);

    if (sctx->argc == 0) {
        efree(sctx);
        return FAILURE;
    }

    if (strcasecmp(kw, "ssubscribe") == 0) {
        zend_hash_internal_pointer_reset(ht_chan);
        if ((z_chan = zend_hash_get_current_data(ht_chan)) == NULL) {
            php_error_docref(NULL, E_WARNING, "Internal Zend HashTable error");
            efree(sctx);
            return FAILURE;
        }
        shardslot = cluster_hash_key_zval(z_chan);
    }

    // Start command construction
    redis_cmd_init_sstr(&cmdstr, sctx->argc, kw, strlen(kw));

    // Iterate over channels
    ZEND_HASH_FOREACH_VAL(ht_chan, z_chan) {
        redis_cmd_append_sstr_key_zval(&cmdstr, z_chan, redis_sock, slot ? &s2 : NULL);

        if (slot && (shardslot != REDIS_CLUSTER_SLOTS && s2 != shardslot)) {
            php_error_docref(NULL, E_WARNING, "All shard channels needs to belong to a single slot");
            smart_string_free(&cmdstr);
            efree(sctx);
            return FAILURE;
        }
    } ZEND_HASH_FOREACH_END();

    // Push values out
    *cmd_len = cmdstr.len;
    *cmd     = cmdstr.c;
    *ctx     = sctx;

    if (shardslot != REDIS_CLUSTER_SLOTS) {
        if (slot) *slot = shardslot;
    } else {
        // Pick a slot at random
        CMD_RAND_SLOT(slot);
    }

    return SUCCESS;
}

/* UNSUBSCRIBE/PUNSUBSCRIBE/SUNSUBSCRIBE */
int redis_unsubscribe_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                          char *kw, char **cmd, int *cmd_len, short *slot,
                          void **ctx)
{
    smart_string cmdstr = {0};
    subscribeContext *sctx;
    HashTable *channels;
    zval *channel;

    ZEND_PARSE_PARAMETERS_START(1, 1)
        Z_PARAM_ARRAY_HT(channels)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    if (zend_hash_num_elements(channels) == 0)
        return FAILURE;

    sctx = ecalloc(1, sizeof(*sctx));
    sctx->kw = kw;
    sctx->argc = zend_hash_num_elements(channels);

    redis_cmd_init_sstr(&cmdstr, sctx->argc, kw, strlen(kw));

    ZEND_HASH_FOREACH_VAL(channels, channel) {
        redis_cmd_append_sstr_key_zval(&cmdstr, channel, redis_sock, slot);
    } ZEND_HASH_FOREACH_END();

    *cmd_len = cmdstr.len;
    *cmd = cmdstr.c;
    *ctx = sctx;

    return SUCCESS;
}

/* ZRANGEBYLEX/ZREVRANGEBYLEX */
int redis_zrangebylex_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                          char *kw, char **cmd, int *cmd_len, short *slot,
                          void **ctx)
{
    char *key, *min, *max;
    size_t key_len, min_len, max_len;
    int argc = ZEND_NUM_ARGS();
    zend_long offset, count;

    /* We need either 3 or 5 arguments for this to be valid */
    if (argc != 3 && argc != 5) {
        php_error_docref(0, E_WARNING, "Must pass either 3 or 5 arguments");
        return FAILURE;
    }

    if (zend_parse_parameters(argc, "sss|ll", &key, &key_len, &min, &min_len,
                             &max, &max_len, &offset, &count) == FAILURE)
    {
        return FAILURE;
    }

    /* min and max must start with '(' or '[', or be either '-' or '+' */
    if (!validate_zlex_arg(min, min_len) || !validate_zlex_arg(max, max_len)) {
        php_error_docref(NULL, E_WARNING,
            "Min/Max args can be '-' or '+', or start with '[' or '('");
        return FAILURE;
    }

    /* Construct command */
    if (argc == 3) {
        *cmd_len = REDIS_CMD_SPPRINTF(cmd, kw, "kss", key, key_len, min, min_len,
            max, max_len);
    } else {
        *cmd_len = REDIS_CMD_SPPRINTF(cmd, kw, "ksssll", key, key_len, min, min_len,
            max, max_len, "LIMIT", 5, offset, count);
    }

    return SUCCESS;
}

/* ZLEXCOUNT/ZREMRANGEBYLEX */
int redis_gen_zlex_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                       char *kw, char **cmd, int *cmd_len, short *slot,
                       void **ctx)
{
    char *key, *min, *max;
    size_t key_len, min_len, max_len;

    /* Parse args */
    if (zend_parse_parameters(ZEND_NUM_ARGS(), "sss", &key, &key_len,
                             &min, &min_len, &max, &max_len) == FAILURE)
    {
        return FAILURE;
    }

    /* Quick sanity check on min/max */
    if (!validate_zlex_arg(min, min_len) || !validate_zlex_arg(max, max_len)) {
        php_error_docref(NULL, E_WARNING,
            "Min/Max args can be '-' or '+', or start with '[' or '('");
        return FAILURE;
    }

    /* Construct command */
    *cmd_len = REDIS_CMD_SPPRINTF(cmd, kw, "kss", key, key_len, min, min_len,
        max, max_len);

    return SUCCESS;
}

/* EVAL and EVALSHA */
int redis_eval_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, char *kw,
                   char **cmd, int *cmd_len, short *slot, void **ctx)
{
    int argc = 0;
    zval *zv;
    HashTable *ht = NULL;
    zend_long num_keys = 0;
    smart_string cmdstr = {0};
    zend_string *arg, *lua, *tmp;
    short prevslot = -1;

    ZEND_PARSE_PARAMETERS_START(1, 3)
        Z_PARAM_STR(lua)
        Z_PARAM_OPTIONAL
        Z_PARAM_ARRAY_HT_OR_NULL(ht)
        Z_PARAM_LONG(num_keys)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    if (ht == NULL)
        ht = (HashTable*)&zend_empty_array;

    argc = zend_hash_num_elements(ht);

    /* EVAL[SHA] {script || sha1} {num keys}  */
    redis_cmd_init_sstr(&cmdstr, 2 + argc, kw, strlen(kw));
    redis_cmd_append_sstr_zstr(&cmdstr, lua);
    redis_cmd_append_sstr_long(&cmdstr, num_keys);

    /* Pick a random slot up front. Any provided key(s) will override this */
    CMD_RAND_SLOT(slot);

    // Iterate over our args if we have any
    ZEND_HASH_FOREACH_VAL(ht, zv) {
        arg = zval_get_tmp_string(zv, &tmp);

        /* If we're still on a key, prefix it check slot */
        if (num_keys-- > 0) {
            redis_cmd_append_sstr_key_zstr(&cmdstr, arg, redis_sock, slot);

            /* If we have been passed a slot, all keys must match */
            if (slot) {
                if (prevslot != -1 && prevslot != *slot) {
                    zend_tmp_string_release(tmp);
                    php_error_docref(0, E_WARNING,
                        "All keys do not map to the same slot");
                    return FAILURE;
                }
                prevslot = *slot;
            }
        } else {
            redis_cmd_append_sstr_zstr(&cmdstr, arg);
        }

        zend_tmp_string_release(tmp);
    } ZEND_HASH_FOREACH_END();

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;
    return SUCCESS;
}

/* Commands that take a key followed by a variable list of serializable
 * values (RPUSH, LPUSH, SADD, SREM, etc...) */
int redis_key_varval_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                         char *kw, char **cmd, int *cmd_len, short *slot,
                         void **ctx)
{
    zval *args = NULL;
    zend_string *key = NULL;
    smart_string cmdstr = {0};
    size_t i;
    int argc = 0;

    ZEND_PARSE_PARAMETERS_START(2, -1)
        Z_PARAM_STR(key)
        Z_PARAM_VARIADIC('*', args, argc)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    /* Initialize our command */
    redis_cmd_init_sstr(&cmdstr, argc + 1, kw, strlen(kw));

    /* Append key */
    redis_cmd_append_sstr_key_zstr(&cmdstr, key, redis_sock, slot);

    /* Add members */
    for (i = 0; i < argc; i++) {
        redis_cmd_append_sstr_zval(&cmdstr, &args[i], redis_sock);
    }

    // Push out values
    *cmd     = cmdstr.c;
    *cmd_len = cmdstr.len;

    // Success!
    return SUCCESS;
}

/* Commands that take a key and then an array of values */
static int gen_key_arr_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                           char *kw, zend_bool pack_values, char **cmd, int *cmd_len,
                           short *slot, void **ctx)
{
    smart_string cmdstr = {0};
    HashTable *values = NULL;
    zend_string *key = NULL;
    zval *zv;

    ZEND_PARSE_PARAMETERS_START(2, 2)
        Z_PARAM_STR(key)
        Z_PARAM_ARRAY_HT(values)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    if (zend_hash_num_elements(values) == 0)
        return FAILURE;

    redis_cmd_init_sstr(&cmdstr, 1 + zend_hash_num_elements(values), kw, strlen(kw));
    redis_cmd_append_sstr_key_zstr(&cmdstr, key, redis_sock, slot);

    ZEND_HASH_FOREACH_VAL(values, zv) {
        redis_cmd_append_sstr_zval(&cmdstr, zv, pack_values ? redis_sock : NULL);
    } ZEND_HASH_FOREACH_END();

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

int redis_key_val_arr_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                        char *kw, char **cmd, int *cmd_len, short *slot,
                        void **ctx)
{
    return gen_key_arr_cmd(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, kw,
        1, cmd, cmd_len, slot, ctx);
}

int redis_key_str_arr_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                        char *kw, char **cmd, int *cmd_len, short *slot,
                        void **ctx)
{
    return gen_key_arr_cmd(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, kw,
        0, cmd, cmd_len, slot, ctx);
}

/* Generic function that takes one or more non-serialized arguments */
static int
gen_vararg_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
               uint32_t min_argc, char *kw, char **cmd, int *cmd_len,
               short *slot, void **ctx)
{
    smart_string cmdstr = {0};
    zval *argv = NULL;
    int argc = 0;
    uint32_t i;

    ZEND_PARSE_PARAMETERS_START(min_argc, -1)
        Z_PARAM_VARIADIC('*', argv, argc)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    redis_cmd_init_sstr(&cmdstr, argc, kw, strlen(kw));

    for (i = 0; i < argc; i++) {
        redis_cmd_append_sstr_zval(&cmdstr, &argv[i], NULL);
    }

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

int redis_mset_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                   char *kw, char **cmd, int *cmd_len, short *slot,
                   void **ctx)
{
    smart_string cmdstr = {0};
    HashTable *kvals = NULL;
    zend_string *key;
    zend_ulong idx;
    zval *zv;

    ZEND_PARSE_PARAMETERS_START(1, 1)
        Z_PARAM_ARRAY_HT(kvals)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    if (zend_hash_num_elements(kvals) == 0)
        return FAILURE;

    redis_cmd_init_sstr(&cmdstr, zend_hash_num_elements(kvals) * 2, kw, strlen(kw));

    ZEND_HASH_FOREACH_KEY_VAL(kvals, idx, key, zv) {
        ZVAL_DEREF(zv);
        if (key) {
            redis_cmd_append_sstr_key_zstr(&cmdstr, key, redis_sock, NULL);
        } else {
            redis_cmd_append_sstr_key_long(&cmdstr, idx, redis_sock, NULL);
        }
        redis_cmd_append_sstr_zval(&cmdstr, zv, redis_sock);
    } ZEND_HASH_FOREACH_END();

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

/* Generic function that takes a variable number of keys, with an optional
 * timeout value.  This can handle various SUNION/SUNIONSTORE/BRPOP type
 * commands. */
static int gen_varkey_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                          char *kw, int kw_len, zend_bool has_timeout,
                          char **cmd, int *cmd_len, short *slot)
{
    zval *argv = NULL, ztimeout = {0}, *zv;
    smart_string cmdstr = {0};
    uint32_t min_argc;
    short kslot = -1;
    int single_array;
    int argc = 0;

    min_argc = has_timeout ? 2 : 1;

    ZEND_PARSE_PARAMETERS_START(min_argc, -1)
        Z_PARAM_VARIADIC('*', argv, argc)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    single_array = argc == min_argc && Z_TYPE(argv[0]) == IS_ARRAY;

    if (has_timeout) {
        if (single_array)
            ZVAL_COPY_VALUE(&ztimeout, &argv[1]);
        else
            ZVAL_COPY_VALUE(&ztimeout, &argv[argc - 1]);

        if (Z_TYPE(ztimeout) != IS_LONG && Z_TYPE(ztimeout) != IS_DOUBLE) {
            php_error_docref(NULL, E_WARNING, "Timeout must be a long or double");
            return FAILURE;
        }
    }

    // If we're running a single array, rework args
    if (single_array) {
        /* Need at least one argument */
        argc = zend_hash_num_elements(Z_ARRVAL(argv[0]));
        if (argc == 0)
            return FAILURE;

        if (has_timeout) argc++;
    }

    // Begin construction of our command
    redis_cmd_init_sstr(&cmdstr, argc, kw, kw_len);

    if (single_array) {
        ZEND_HASH_FOREACH_VAL(Z_ARRVAL(argv[0]), zv) {
            redis_cmd_append_sstr_key_zval(&cmdstr, zv, redis_sock, slot);
            if (slot) {
                if (kslot != -1 && *slot != kslot)
                    goto cross_slot;
                kslot = *slot;
            }
        } ZEND_HASH_FOREACH_END();
    } else {
        uint32_t i;
        for(i = 0; i < argc - !!has_timeout; i++) {
            redis_cmd_append_sstr_key_zval(&cmdstr, &argv[i], redis_sock, slot);
            if (slot) {
                if (kslot != -1 && *slot != kslot)
                    goto cross_slot;
                kslot = *slot;
            }
        }
    }

    if (Z_TYPE(ztimeout) == IS_DOUBLE) {
        redis_cmd_append_sstr_dbl(&cmdstr, Z_DVAL(ztimeout));
    } else if (Z_TYPE(ztimeout) == IS_LONG) {
        redis_cmd_append_sstr_long(&cmdstr, Z_LVAL(ztimeout));
    }

    // Push out parameters
    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;

cross_slot:
    efree(cmdstr.c);
    php_error_docref(NULL, E_WARNING, "Not all keys hash to the same slot!");
    return FAILURE;
}

int redis_mpop_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, char *kw,
                   char **cmd, int *cmd_len, short *slot, void **ctx)
{
    int argc, blocking, is_zmpop;
    smart_string cmdstr = {0};
    zend_string *from = NULL;
    HashTable *keys = NULL;
    double timeout = 0.0;
    zend_long count = 1;
    short slot2 = -1;
    zval *zv;

    /* Sanity check on our keyword */
    ZEND_ASSERT(kw != NULL && *kw != '\0' && *(kw+1) != '\0');

    blocking = tolower(*kw) == 'b';
    is_zmpop = tolower(kw[blocking]) == 'z';

    ZEND_PARSE_PARAMETERS_START(2 + blocking, 3 + blocking) {
        if (blocking) {
            Z_PARAM_DOUBLE(timeout)
        }
        Z_PARAM_ARRAY_HT(keys)
        Z_PARAM_STR(from);
        Z_PARAM_OPTIONAL
        Z_PARAM_LONG(count);
    } ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    if (zend_hash_num_elements(keys) == 0) {
        php_error_docref(NULL, E_WARNING, "Must pass at least one key");
        return FAILURE;
    } else if (count < 1) {
        php_error_docref(NULL, E_WARNING, "Count must be > 0");
        return FAILURE;
    } else if (!is_zmpop && !(zend_string_equals_literal_ci(from, "LEFT") ||
                              zend_string_equals_literal_ci(from, "RIGHT")))
    {
        php_error_docref(NULL, E_WARNING, "from must be either 'LEFT' or 'RIGHT'");
        return FAILURE;
    } else if (is_zmpop && !(zend_string_equals_literal_ci(from, "MIN") ||
                             zend_string_equals_literal_ci(from, "MAX")))
    {
        php_error_docref(NULL, E_WARNING, "from must be either 'MIN' or 'MAX'");
        return FAILURE;
    }

    argc = 2 + !!blocking + zend_hash_num_elements(keys) + (count != 1 ? 2 : 0);
    redis_cmd_init_sstr(&cmdstr, argc, kw, strlen(kw));

    if (blocking) redis_cmd_append_sstr_dbl(&cmdstr, timeout);
    redis_cmd_append_sstr_long(&cmdstr, zend_hash_num_elements(keys));

    if (slot) *slot = -1;

    ZEND_HASH_FOREACH_VAL(keys, zv) {
        redis_cmd_append_sstr_key_zval(&cmdstr, zv, redis_sock, slot);
        if (slot) {
            if (slot2 != -1 && *slot != slot2) {
                php_error_docref(NULL, E_WARNING, "All keys don't hash to the same slot");
                efree(cmdstr.c);
                return FAILURE;
            }
            slot2 = *slot;
        }
    } ZEND_HASH_FOREACH_END();

    redis_cmd_append_sstr_zstr(&cmdstr, from);

    if (count != 1) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "COUNT");
        redis_cmd_append_sstr_long(&cmdstr, count);
    }

    *ctx = is_zmpop ? PHPREDIS_CTX_PTR : NULL;
    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

int redis_info_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                   char **cmd, int *cmd_len, short *slot, void **ctx)
{
    return gen_vararg_cmd(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, 0,
                          "INFO", cmd, cmd_len, slot, ctx);
}

int redis_script_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                   char **cmd, int *cmd_len, short *slot, void **ctx)
{
    int argc = 0;
    smart_string cmdstr = {0};
    zval *argv = NULL;

    ZEND_PARSE_PARAMETERS_START(1, -1)
        Z_PARAM_VARIADIC('*', argv, argc)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    if (redis_build_script_cmd(&cmdstr, argc, argv) == NULL) {
        return FAILURE;
    }

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

/* Generic handling of every blocking pop command (BLPOP, BZPOP[MIN/MAX], etc */
int redis_blocking_pop_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                           char *kw, char **cmd, int *cmd_len, short *slot,
                           void **ctx)
{
    return gen_varkey_cmd(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, kw,
        strlen(kw), 1, cmd, cmd_len, slot);
}

/*
 * Commands with specific signatures or that need unique functions because they
 * have specific processing (argument validation, etc) that make them unique
 */

int
redis_pop_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
              char *kw, char **cmd, int *cmd_len, short *slot, void **ctx)
{
    char *key;
    size_t key_len;
    smart_string cmdstr = {0};
    zend_long count = 0;

    // Make sure the function is being called correctly
    if (zend_parse_parameters(ZEND_NUM_ARGS(), "s|l",
                              &key, &key_len, &count) == FAILURE)
    {
        return FAILURE;
    }

    redis_cmd_init_sstr(&cmdstr, 1 + (count > 0), kw, strlen(kw));
    redis_cmd_append_sstr_key(&cmdstr, key, key_len, redis_sock, slot);
    if (count > 0) {
        redis_cmd_append_sstr_long(&cmdstr, (long)count);
        *ctx = PHPREDIS_CTX_PTR;
    }

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

int
redis_acl_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
              char **cmd, int *cmd_len, short *slot, void **ctx)
{
    smart_string cmdstr = {0};
    zend_string *op, *zstr;
    zval *z_args = NULL;
    int argc = 0, i;

    ZEND_PARSE_PARAMETERS_START(1, -1)
        Z_PARAM_STR(op)
        Z_PARAM_OPTIONAL
        Z_PARAM_VARIADIC('*', z_args, argc)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    if (zend_string_equals_literal_ci(op, "CAT") ||
        zend_string_equals_literal_ci(op, "LIST") ||
        zend_string_equals_literal_ci(op, "USERS")
    ) {
        *ctx = NULL;
    } else if (zend_string_equals_literal_ci(op, "LOAD") ||
        zend_string_equals_literal_ci(op, "SAVE")
    ) {
        *ctx = PHPREDIS_CTX_PTR;
    } else if (zend_string_equals_literal_ci(op, "GENPASS") ||
        zend_string_equals_literal_ci(op, "WHOAMI")
    ) {
        *ctx = PHPREDIS_CTX_PTR + 1;
    } else if (zend_string_equals_literal_ci(op, "SETUSER")) {
        if (argc < 1) {
            php_error_docref(NULL, E_WARNING, "ACL SETUSER requires at least one argument");
            return FAILURE;
        }
        *ctx = PHPREDIS_CTX_PTR;
    } else if (zend_string_equals_literal_ci(op, "DELUSER")) {
        if (argc < 1) {
            php_error_docref(NULL, E_WARNING, "ACL DELUSER requires at least one argument");
            return FAILURE;
        }
        *ctx = PHPREDIS_CTX_PTR + 2;
    } else if (zend_string_equals_literal_ci(op, "GETUSER")) {
        if (argc < 1) {
            php_error_docref(NULL, E_WARNING, "ACL GETUSER requires at least one argument");
            return FAILURE;
        }
        *ctx = PHPREDIS_CTX_PTR + 3;
    } else if (zend_string_equals_literal_ci(op, "DRYRUN")) {
        if (argc < 2) {
            php_error_docref(NULL, E_WARNING, "ACL DRYRUN requires at least two arguments");
            return FAILURE;
        }
        *ctx = PHPREDIS_CTX_PTR;
    } else if (zend_string_equals_literal_ci(op, "LOG")) {
        if (argc > 0 && Z_TYPE(z_args[0]) == IS_STRING && ZVAL_STRICMP_STATIC(&z_args[0], "RESET")) {
            *ctx = PHPREDIS_CTX_PTR;
        } else {
            *ctx = PHPREDIS_CTX_PTR + 4;
        }
    } else {
        php_error_docref(NULL, E_WARNING, "Unknown ACL operation '%s'", ZSTR_VAL(op));
        return FAILURE;
    }

    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 1 + argc, "ACL");
    redis_cmd_append_sstr_zstr(&cmdstr, op);

    for (i = 0; i < argc; ++i) {
        zstr = zval_get_string(&z_args[i]);
        redis_cmd_append_sstr_zstr(&cmdstr, zstr);
        zend_string_release(zstr);
    }

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

int redis_waitaof_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                      char **cmd, int *cmd_len, short *slot, void **ctx)
{
    zend_long numlocal, numreplicas, timeout;
    smart_string cmdstr = {0};

    ZEND_PARSE_PARAMETERS_START(3, 3)
        Z_PARAM_LONG(numlocal)
        Z_PARAM_LONG(numreplicas)
        Z_PARAM_LONG(timeout)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    if (numlocal < 0 || numreplicas < 0 || timeout < 0) {
        php_error_docref(NULL, E_WARNING, "No arguments can be negative");
        return FAILURE;
    }

    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 3, "WAITAOF");
    redis_cmd_append_sstr_long(&cmdstr, numlocal);
    redis_cmd_append_sstr_long(&cmdstr, numreplicas);
    redis_cmd_append_sstr_long(&cmdstr, timeout);

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

/* Attempt to pull a long expiry from a zval.  We're more restrictave than zval_get_long
 * because that function will return integers from things like open file descriptors
 * which should simply fail as a TTL */
static int redis_try_get_expiry(zval *zv, zend_long *lval) {
    double dval;

    /* Success on an actual long or double */
    if (Z_TYPE_P(zv) == IS_LONG || Z_TYPE_P(zv) == IS_DOUBLE) {
        *lval = zval_get_long(zv);
        return SUCCESS;
    }

    /* Automatically fail if we're not a string */
    if (Z_TYPE_P(zv) != IS_STRING)
        return FAILURE;

    /* Attempt to get a long from the string */
    switch (is_numeric_string(Z_STRVAL_P(zv), Z_STRLEN_P(zv), lval, &dval, 0)) {
        case IS_DOUBLE:
            *lval = dval;
            REDIS_FALLTHROUGH;
        case IS_LONG:
            return SUCCESS;
        default:
            return FAILURE;
    }
}

/* SET */
int redis_set_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                  char **cmd, int *cmd_len, short *slot, void **ctx)
{
    char *key = NULL, *exp_type = NULL, *set_type = NULL;
    zval *z_value, *z_opts = NULL, *ifeq = NULL;
    zend_string *zstr = NULL, *tmp = NULL;
    smart_string cmdstr = {0};
    zend_long expire = -1;
    zend_bool get = 0;
    long keep_ttl = 0;
    size_t key_len;

    #define setExpiryWarning(zv) \
        php_error_docref(NULL, E_WARNING, "%s passed as EXPIRY is invalid " \
                         "(must be an int, float, or numeric string >= 1)", \
                         zend_zval_type_name((zv)))

    // Make sure the function is being called correctly
    if (zend_parse_parameters(ZEND_NUM_ARGS(), "sz|z", &key, &key_len,
                             &z_value, &z_opts) == FAILURE)
    {
        return FAILURE;
    }

    // Check for an options array
    if (z_opts && Z_TYPE_P(z_opts) == IS_ARRAY) {
        HashTable *kt = Z_ARRVAL_P(z_opts);
        zend_string *zkey;
        zval *v;

        /* Iterate our option array */
        ZEND_HASH_FOREACH_STR_KEY_VAL(kt, zkey, v) {
            ZVAL_DEREF(v);
            /* Detect PX or EX argument and validate timeout */
            if (zkey && (zend_string_equals_literal_ci(zkey, "EX") ||
                         zend_string_equals_literal_ci(zkey, "PX") ||
                         zend_string_equals_literal_ci(zkey, "EXAT") ||
                         zend_string_equals_literal_ci(zkey, "PXAT"))
            ) {
                if (redis_try_get_expiry(v, &expire) == FAILURE || expire < 1) {
                    setExpiryWarning(v);
                    return FAILURE;
                }

                exp_type = ZSTR_VAL(zkey);
            } else if (zkey && zend_string_equals_literal_ci(zkey, "IFEQ")) {
                ifeq = v;
            } else if (Z_TYPE_P(v) == IS_STRING) {
                if (zend_string_equals_literal_ci(Z_STR_P(v), "KEEPTTL")) {
                    keep_ttl  = 1;
                } else if (zend_string_equals_literal_ci(Z_STR_P(v), "GET")) {
                    get = 1;
                } else if (zend_string_equals_literal_ci(Z_STR_P(v), "NX") ||
                           zend_string_equals_literal_ci(Z_STR_P(v), "XX"))
                {
                    set_type = Z_STRVAL_P(v);
                }
            }
        } ZEND_HASH_FOREACH_END();
    } else if (z_opts && Z_TYPE_P(z_opts) != IS_NULL) {
        if (redis_try_get_expiry(z_opts, &expire) == FAILURE || expire < 1) {
            setExpiryWarning(z_opts);
            return FAILURE;
        }
    }

    /* Protect the user from syntax errors but give them some info about what's wrong */
    if (exp_type && keep_ttl) {
        php_error_docref(NULL, E_WARNING, "KEEPTTL can't be combined with EX or PX option");
        return FAILURE;
    }

    /* You can't use IFEQ with NX or XX */
    if (set_type && ifeq) {
        php_error_docref(NULL, E_WARNING, "IFEQ can't be combined with NX or XX option");
        return FAILURE;
    }

    /* Backward compatibility:  If we are passed no options except an EXPIRE ttl, we
     * actually execute a SETEX command */
    if (expire > 0 && !exp_type && !set_type && !keep_ttl) {
        *cmd_len = REDIS_CMD_SPPRINTF(cmd, "SETEX", "klv", key, key_len, expire, z_value);
        return SUCCESS;
    }

    /* Calculate argc based on options set */
    int argc = 2 + (ifeq ? 2 : 0) + (exp_type ? 2 : 0) + (set_type != NULL) +
        (keep_ttl != 0) + get;

    /* Initial SET <key> <value> */
    redis_cmd_init_sstr(&cmdstr, argc, ZEND_STRL("SET"));
    redis_cmd_append_sstr_key(&cmdstr, key, key_len, redis_sock, slot);
    redis_cmd_append_sstr_zval(&cmdstr, z_value, redis_sock);

    if (ifeq) {
        zstr = zval_get_tmp_string(ifeq, &tmp);
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "IFEQ");
        redis_cmd_append_sstr_zstr(&cmdstr, zstr);
        zend_tmp_string_release(tmp);
    } else if (set_type) {
        redis_cmd_append_sstr(&cmdstr, set_type, strlen(set_type));
    }

    if (get) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "GET");
        *ctx = PHPREDIS_CTX_PTR;
    }

    if (exp_type) {
        redis_cmd_append_sstr(&cmdstr, exp_type, strlen(exp_type));
        redis_cmd_append_sstr_long(&cmdstr, (long)expire);
    } else if (keep_ttl) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "KEEPTTL");
    }

    /* Push command and length to the caller */
    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;

    #undef setExpiryWarning
}

/* MGET */
int redis_mget_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                   char **cmd, int *cmd_len, short *slot, void **ctx)
{
    smart_string cmdstr = {0};
    HashTable *keys = NULL;
    zval *zkey;

    /* RedisCluster has a custom MGET implementation */
    ZEND_ASSERT(slot == NULL);

    ZEND_PARSE_PARAMETERS_START(1, 1)
        Z_PARAM_ARRAY_HT(keys)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    if (zend_hash_num_elements(keys) == 0)
        return FAILURE;

    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, zend_hash_num_elements(keys), "MGET");

    ZEND_HASH_FOREACH_VAL(keys, zkey) {
        ZVAL_DEREF(zkey);
        redis_cmd_append_sstr_key_zval(&cmdstr, zkey, redis_sock, slot);
    } ZEND_HASH_FOREACH_END();

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

int
redis_getex_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                char **cmd, int *cmd_len, short *slot, void **ctx)
{
    smart_string cmdstr = {0};
    char *key, *exp_type = NULL;
    zval *z_opts = NULL, *z_ele;
    zend_long expire = -1;
    zend_bool persist = 0;
    zend_string *zkey;
    size_t key_len;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "s|a",
                              &key, &key_len, &z_opts) == FAILURE)
    {
        return FAILURE;
    }

    if (z_opts != NULL) {
        ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(z_opts), zkey, z_ele) {
            if (zkey != NULL) {
                ZVAL_DEREF(z_ele);
                if (ZSTR_STRICMP_STATIC(zkey, "EX") ||
                    ZSTR_STRICMP_STATIC(zkey, "PX") ||
                    ZSTR_STRICMP_STATIC(zkey, "EXAT") ||
                    ZSTR_STRICMP_STATIC(zkey, "PXAT")
                ) {
                    exp_type = ZSTR_VAL(zkey);
                    expire = zval_get_long(z_ele);
                    persist = 0;
                } else if (ZSTR_STRICMP_STATIC(zkey, "PERSIST")) {
                    persist = zval_is_true(z_ele);
                    exp_type = NULL;
                }
            } else if (Z_TYPE_P(z_ele) == IS_STRING &&
                       zend_string_equals_literal_ci(Z_STR_P(z_ele), "PERSIST"))
            {
                persist = zval_is_true(z_ele);
                exp_type = NULL;
            }
        } ZEND_HASH_FOREACH_END();
    }

    if (exp_type != NULL && expire < 1) {
        php_error_docref(NULL, E_WARNING, "EXPIRE can't be < 1");
        return FAILURE;
    }

    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 1 + (exp_type ? 2 : persist), "GETEX");
    redis_cmd_append_sstr_key(&cmdstr, key, key_len, redis_sock, slot);

    if (exp_type != NULL) {
        redis_cmd_append_sstr(&cmdstr, exp_type, strlen(exp_type));
        redis_cmd_append_sstr_long(&cmdstr, expire);
    } else if (persist) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "PERSIST");
    }

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

/* BRPOPLPUSH */
int redis_brpoplpush_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                         char **cmd, int *cmd_len, short *slot, void **ctx)
{
    zend_string *src = NULL, *dst = NULL;
    double timeout = 0;

    ZEND_PARSE_PARAMETERS_START(3, 3)
        Z_PARAM_STR(src)
        Z_PARAM_STR(dst)
        Z_PARAM_DOUBLE(timeout)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    src = redis_key_prefix_zstr(redis_sock, src);
    dst = redis_key_prefix_zstr(redis_sock, dst);

    if (slot && (*slot = cluster_hash_key_zstr(src)) != cluster_hash_key_zstr(dst)) {
        php_error_docref(NULL, E_WARNING, "Keys must hash to the same slot");
        zend_string_release(src);
        zend_string_release(dst);
        return FAILURE;
    }

    /* Consistency with Redis.  If timeout < 0 use RPOPLPUSH */
    if (timeout < 0) {
        *cmd_len = REDIS_CMD_SPPRINTF(cmd, "RPOPLPUSH", "SS", src, dst);
    } else if (fabs(timeout - (long)timeout) < .0001) {
        *cmd_len = REDIS_CMD_SPPRINTF(cmd, "BRPOPLPUSH", "SSd", src, dst, (long)timeout);
    } else {
        *cmd_len = REDIS_CMD_SPPRINTF(cmd, "BRPOPLPUSH", "SSf", src, dst, timeout);
    }

    zend_string_release(src);
    zend_string_release(dst);

    return SUCCESS;
}

/* To maintain backward compatibility with earlier versions of phpredis, we
 * allow for an optional "increment by" argument for INCR and DECR even though
 * that's not how Redis proper works */
#define TYPE_INCR 0
#define TYPE_DECR 1

/* Handle INCR(BY) and DECR(BY) depending on optional increment value */
static int
redis_atomic_increment(INTERNAL_FUNCTION_PARAMETERS, int type,
                       RedisSock *redis_sock, char **cmd, int *cmd_len,
                       short *slot, void **ctx)
{
    char *key;
    size_t key_len;
    zend_long val = 1;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "s|l", &key, &key_len,
                              &val) == FAILURE)
    {
        return FAILURE;
    }

    /* If our value is 1 we use INCR/DECR.  For other values, treat the call as
     * an INCRBY or DECRBY call */
    if (type == TYPE_INCR) {
        if (val == 1) {
            *cmd_len = REDIS_CMD_SPPRINTF(cmd, "INCR", "k", key, key_len);
        } else {
            *cmd_len = REDIS_CMD_SPPRINTF(cmd, "INCRBY", "kl", key, key_len, val);
        }
    } else {
        if (val == 1) {
            *cmd_len = REDIS_CMD_SPPRINTF(cmd, "DECR", "k", key, key_len);
        } else {
            *cmd_len = REDIS_CMD_SPPRINTF(cmd, "DECRBY", "kl", key, key_len, val);
        }
    }

    /* Success */
    return SUCCESS;
}

/* INCR */
int redis_incr_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                   char **cmd, int *cmd_len, short *slot, void **ctx)
{
    return redis_atomic_increment(INTERNAL_FUNCTION_PARAM_PASSTHRU,
        TYPE_INCR, redis_sock, cmd, cmd_len, slot, ctx);
}

/* DECR */
int redis_decr_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                   char **cmd, int *cmd_len, short *slot, void **ctx)
{
    return redis_atomic_increment(INTERNAL_FUNCTION_PARAM_PASSTHRU,
        TYPE_DECR, redis_sock, cmd, cmd_len, slot, ctx);
}

/* HINCRBY */
int redis_hincrby_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                      char **cmd, int *cmd_len, short *slot, void **ctx)
{
    char *key, *mem;
    size_t key_len, mem_len;
    zend_long byval;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "ssl", &key, &key_len,
                             &mem, &mem_len, &byval) == FAILURE)
    {
        return FAILURE;
    }

    // Construct command
    *cmd_len = REDIS_CMD_SPPRINTF(cmd, "HINCRBY", "ksl", key, key_len, mem, mem_len, byval);

    // Success
    return SUCCESS;
}

/* HINCRBYFLOAT */
int redis_hincrbyfloat_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                           char **cmd, int *cmd_len, short *slot, void **ctx)
{
    char *key, *mem;
    size_t key_len, mem_len;
    double byval;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "ssd", &key, &key_len,
                             &mem, &mem_len, &byval) == FAILURE)
    {
        return FAILURE;
    }

    // Construct command
    *cmd_len = REDIS_CMD_SPPRINTF(cmd, "HINCRBYFLOAT", "ksf", key, key_len, mem,
                                 mem_len, byval);

    // Success
    return SUCCESS;
}

static inline zval *coerce_hash_field(zval *zv, zval *aux) {
    zend_long lv;

    if (UNEXPECTED(Z_TYPE_P(zv) == IS_STRING &&
                   is_numeric_string(Z_STRVAL_P(zv),
                                     Z_STRLEN_P(zv), &lv, NULL, 0) == IS_LONG))
    {
        ZVAL_LONG(aux, lv);
        return aux;
    }

    return zv;
}

/* A helper function to build HMGET, HGETEX, HGETDEL context arrays we curry
 * to the reply side to return to the user fields and values */
static HashTable *
build_hash_context_ht(HashTable *htsrc, zend_bool (*cb)(zval*)) {
    zend_string *key, *tmp;
    HashTable *ht;
    zval *zv, aux;

    ALLOC_HASHTABLE(ht);
    zend_hash_init(ht, zend_hash_num_elements(htsrc),
                   NULL, ZVAL_PTR_DTOR, 0);

    ZEND_HASH_FOREACH_VAL(htsrc, zv)
        ZVAL_DEREF(zv);
        if (cb && !cb(zv))
            continue;

        /* Legacy behavior: Integer strings become integer keys */
        zv = coerce_hash_field(zv, &aux);

        if (Z_TYPE_P(zv) == IS_LONG) {
            zend_hash_index_add_empty_element(ht, Z_LVAL_P(zv));
        } else {
            key = zval_get_tmp_string(zv, &tmp);
            zend_hash_add_empty_element(ht, key);
            zend_tmp_string_release(tmp);
        }
    ZEND_HASH_FOREACH_END();

    /* Sanity check that we have at least one value */
    if (zend_hash_num_elements(ht) == 0) {
        zend_hash_destroy(ht);
        FREE_HASHTABLE(ht);
        return NULL;
    }

    return ht;
}

/* Legacy HMGET behavior predicate */
static zend_bool hmget_filter(zval *zv) {
    return (Z_TYPE_P(zv) == IS_STRING && Z_STRLEN_P(zv) > 0) ||
           (Z_TYPE_P(zv) == IS_LONG);
}

static void
redis_cmd_append_sstr_hash_fields(smart_string *cmdstr, HashTable *ht) {
    zend_string *key;
    zend_ulong idx;

    ZEND_HASH_FOREACH_KEY(ht, idx, key) {
        if (key) {
            redis_cmd_append_sstr_zstr(cmdstr, key);
        } else {
            redis_cmd_append_sstr_long(cmdstr, idx);
        }
    } ZEND_HASH_FOREACH_END();

}

/* HMGET */
int redis_hmget_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                    char **cmd, int *cmd_len, short *slot, void **ctx)
{
    HashTable *fields = NULL, *htctx = NULL;
    smart_string cmdstr = {0};
    zend_string *key = NULL;
    int argc;

    ZEND_PARSE_PARAMETERS_START(2, 2)
        Z_PARAM_STR(key)
        Z_PARAM_ARRAY_HT(fields)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    if (zend_hash_num_elements(fields) == 0)
        return FAILURE;

    htctx = build_hash_context_ht(fields, hmget_filter);
    if (htctx == NULL) {
        php_error_docref(NULL, E_WARNING, "No valid fields provided");
        return FAILURE;
    }

    argc = 1 + zend_hash_num_elements(htctx);
    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, argc, "HMGET");
    redis_cmd_append_sstr_key_zstr(&cmdstr, key, redis_sock, slot);

    redis_cmd_append_sstr_hash_fields(&cmdstr, htctx);

    // Push out command, length, and key context
    *cmd     = cmdstr.c;
    *cmd_len = cmdstr.len;
    *ctx     = htctx;

    return SUCCESS;
}

/* HMSET */
int redis_hmset_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                    char **cmd, int *cmd_len, short *slot, void **ctx)
{
    smart_string cmdstr = {0};
    zend_string *key = NULL;
    HashTable *ht = NULL;
    uint32_t fields;
    zend_ulong idx;
    zval *zv;

    ZEND_PARSE_PARAMETERS_START(2, 2)
        Z_PARAM_STR(key)
        Z_PARAM_ARRAY_HT(ht)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    fields = zend_hash_num_elements(ht);
    if (fields == 0)
        return FAILURE;

    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 1 + (2 * fields), "HMSET");
    redis_cmd_append_sstr_key_zstr(&cmdstr, key, redis_sock, slot);

    ZEND_HASH_FOREACH_KEY_VAL(ht, idx, key, zv) {
        if (key) {
            redis_cmd_append_sstr_zstr(&cmdstr, key);
        } else {
            redis_cmd_append_sstr_long(&cmdstr, idx);
        }
        redis_cmd_append_sstr_zval(&cmdstr, zv, redis_sock);
    } ZEND_HASH_FOREACH_END();

    *cmd_len = cmdstr.len;
    *cmd = cmdstr.c;

    return SUCCESS;
}

/* HSTRLEN */
int
redis_hstrlen_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                  char **cmd, int *cmd_len, short *slot, void **ctx)
{
    char *key, *field;
    size_t key_len, field_len;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "ss", &key, &key_len,
                              &field, &field_len) == FAILURE
    ) {
        return FAILURE;
    }

    *cmd_len = REDIS_CMD_SPPRINTF(cmd, "HSTRLEN", "ks", key, key_len, field, field_len);

    return SUCCESS;
}

static void redis_get_lcs_options(redisLcsOptions *dst, HashTable *ht) {
    zend_string *key;
    zval *zv;

    ZEND_ASSERT(dst != NULL);

    memset(dst, 0, sizeof(*dst));

    if (ht == NULL)
        return;

    ZEND_HASH_FOREACH_STR_KEY_VAL(ht, key, zv) {
        if (key) {
            if (zend_string_equals_literal_ci(key, "LEN")) {
                dst->idx = 0;
                dst->len = zval_is_true(zv);
            } else if (zend_string_equals_literal_ci(key, "IDX")) {
                dst->len = 0;
                dst->idx = zval_is_true(zv);
            } else if (zend_string_equals_literal_ci(key, "MINMATCHLEN")) {
                dst->minmatchlen = zval_get_long(zv);
            } else if (zend_string_equals_literal_ci(key, "WITHMATCHLEN")) {
                dst->withmatchlen = zval_is_true(zv);
            } else {
                php_error_docref(NULL, E_WARNING, "Unknown LCS option '%s'", ZSTR_VAL(key));
            }
        } else if (Z_TYPE_P(zv) == IS_STRING) {
            if (zend_string_equals_literal_ci(Z_STR_P(zv), "LEN")) {
                dst->idx = 0;
                dst->len = 1;
            } else if (zend_string_equals_literal_ci(Z_STR_P(zv), "IDX")) {
                dst->idx = 1;
                dst->len = 0;
            } else if (zend_string_equals_literal_ci(Z_STR_P(zv), "WITHMATCHLEN")) {
                dst->withmatchlen = 1;
            }
        }
    } ZEND_HASH_FOREACH_END();
}

/* LCS */
int redis_lcs_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                  char **cmd, int *cmd_len, short *slot, void **ctx)
{
    zend_string *key1 = NULL, *key2 = NULL;
    smart_string cmdstr = {0};
    HashTable *ht = NULL;
    redisLcsOptions opt;
    int argc;

    ZEND_PARSE_PARAMETERS_START(2, 3)
        Z_PARAM_STR(key1)
        Z_PARAM_STR(key2)
        Z_PARAM_OPTIONAL
        Z_PARAM_ARRAY_HT_OR_NULL(ht)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    key1 = redis_key_prefix_zstr(redis_sock, key1);
    key2 = redis_key_prefix_zstr(redis_sock, key2);

    if (slot) {
        *slot = cluster_hash_key_zstr(key1);
        if (*slot != cluster_hash_key_zstr(key2)) {
            php_error_docref(NULL, E_WARNING, "Warning, not all keys hash to the same slot!");
            zend_string_release(key1);
            zend_string_release(key2);
            return FAILURE;
        }
    }

    redis_get_lcs_options(&opt, ht);

    argc = 2 + !!opt.idx + !!opt.len + !!opt.withmatchlen + (opt.minmatchlen ? 2 : 0);
    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, argc, "LCS");

    redis_cmd_append_sstr_zstr(&cmdstr, key1);
    redis_cmd_append_sstr_zstr(&cmdstr, key2);

    REDIS_CMD_APPEND_SSTR_OPT_STATIC(&cmdstr, opt.idx, "IDX");
    REDIS_CMD_APPEND_SSTR_OPT_STATIC(&cmdstr, opt.len, "LEN");
    REDIS_CMD_APPEND_SSTR_OPT_STATIC(&cmdstr, opt.withmatchlen, "WITHMATCHLEN");

    if (opt.minmatchlen) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "MINMATCHLEN");
        redis_cmd_append_sstr_long(&cmdstr, opt.minmatchlen);
    }

    zend_string_release(key1);
    zend_string_release(key2);

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;
    return SUCCESS;
}

int redis_slowlog_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                      char **cmd, int *cmd_len, short *slot, void **ctx)
{
    enum {SLOWLOG_GET, SLOWLOG_LEN, SLOWLOG_RESET} mode;
    smart_string cmdstr = {0};
    zend_string *op = NULL;
    zend_long arg = 0;

    ZEND_PARSE_PARAMETERS_START(1, 2)
        Z_PARAM_STR(op)
        Z_PARAM_OPTIONAL
        Z_PARAM_LONG(arg)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    if (zend_string_equals_literal_ci(op, "GET")) {
        mode = SLOWLOG_GET;
    } else if (zend_string_equals_literal_ci(op, "LEN")) {
        mode = SLOWLOG_LEN;
    } else if (zend_string_equals_literal_ci(op, "RESET")) {
        mode = SLOWLOG_RESET;
    } else {
        php_error_docref(NULL, E_WARNING, "Unknown SLOWLOG operation '%s'", ZSTR_VAL(op));
        return FAILURE;
    }

    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 1 + (mode == SLOWLOG_GET && ZEND_NUM_ARGS() == 2), "SLOWLOG");
    redis_cmd_append_sstr_zstr(&cmdstr, op);

    if (mode == SLOWLOG_GET && ZEND_NUM_ARGS() == 2)
        redis_cmd_append_sstr_long(&cmdstr, arg);

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

void redis_get_restore_options(redisRestoreOptions *dst, HashTable *ht) {
    zend_string *key;
    zend_long lval;
    zval *zv;

    ZEND_ASSERT(dst != NULL);

    memset(dst, 0, sizeof(*dst));
    dst->idletime = dst->freq = -1;

    if (ht == NULL)
        return;

    ZEND_HASH_FOREACH_STR_KEY_VAL(ht, key, zv) {
        ZVAL_DEREF(zv);

        if (key) {
            if (zend_string_equals_literal_ci(key, "IDLETIME")) {
                lval = zval_get_long(zv);
                if (lval < 0) {
                    php_error_docref(NULL, E_WARNING, "IDLETIME must be >= 0");
                } else {
                    dst->idletime = lval;
                    dst->freq = -1;
                }
            } else if (zend_string_equals_literal_ci(key, "FREQ")) {
                lval = zval_get_long(zv);
                if (lval < 0 || lval > 255) {
                    php_error_docref(NULL, E_WARNING, "FREQ must be >= 0 and <= 255");
                } else {
                    dst->freq = lval;
                    dst->idletime = -1;
                }
            } else {
                php_error_docref(NULL, E_WARNING, "Unknown RESTORE option '%s'", ZSTR_VAL(key));
            }
        } else if (Z_TYPE_P(zv) == IS_STRING) {
            if (zend_string_equals_literal_ci(Z_STR_P(zv), "REPLACE")) {
                dst->replace = 1;
            } else if (zend_string_equals_literal_ci(Z_STR_P(zv), "ABSTTL")) {
                dst->absttl = 1;
            } else {
                php_error_docref(NULL, E_WARNING, "Unknown RESTORE option '%s'", Z_STRVAL_P(zv));
            }
        }
    } ZEND_HASH_FOREACH_END();
}

/* RESTORE */
int redis_restore_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                      char **cmd, int *cmd_len, short *slot, void **ctx)
{
    zend_string *key, *value = NULL;
    smart_string cmdstr = {0};
    HashTable *options = NULL;
    redisRestoreOptions opt;
    zend_long timeout = 0;
    int argc;

    ZEND_PARSE_PARAMETERS_START(3, 4) {
        Z_PARAM_STR(key)
        Z_PARAM_LONG(timeout)
        Z_PARAM_STR(value)
        Z_PARAM_OPTIONAL
        Z_PARAM_ARRAY_HT_OR_NULL(options)
    } ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    redis_get_restore_options(&opt, options);

    argc = 3 + (opt.idletime>-1?2:0) + (opt.freq>-1?2:0) + !!opt.absttl + !!opt.replace;
    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, argc, "RESTORE");

    redis_cmd_append_sstr_key(&cmdstr, ZSTR_VAL(key), ZSTR_LEN(key), redis_sock, slot);
    redis_cmd_append_sstr_long(&cmdstr, timeout);
    redis_cmd_append_sstr_zstr(&cmdstr, value);

    REDIS_CMD_APPEND_SSTR_OPT_STATIC(&cmdstr, opt.replace, "REPLACE");
    REDIS_CMD_APPEND_SSTR_OPT_STATIC(&cmdstr, opt.absttl, "ABSTTL");

    if (opt.idletime > -1) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "IDLETIME");
        redis_cmd_append_sstr_long(&cmdstr, opt.idletime);
    }

    if (opt.freq > -1) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "FREQ");
        redis_cmd_append_sstr_long(&cmdstr, opt.freq);
    }

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

/* BITPOS key bit [start [end [BYTE | BIT]]] */
int redis_bitpos_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                     char **cmd, int *cmd_len, short *slot, void **ctx)
{
    zend_long start = 0, end = -1;
    zend_bool bit = 0, bybit = 0;
    smart_string cmdstr = {0};
    zend_string *key = NULL;

    ZEND_PARSE_PARAMETERS_START(2, 5)
        Z_PARAM_STR(key)
        Z_PARAM_BOOL(bit)
        Z_PARAM_OPTIONAL
        Z_PARAM_LONG(start)
        Z_PARAM_LONG(end)
        Z_PARAM_BOOL(bybit)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 2 + (ZEND_NUM_ARGS() > 2 ? 2 : 0) + !!bybit, "BITPOS");

    redis_cmd_append_sstr_key_zstr(&cmdstr, key, redis_sock, slot);
    redis_cmd_append_sstr_long(&cmdstr, bit);

    /* Start and length if we were passed either */
    if (ZEND_NUM_ARGS() > 2) {
        redis_cmd_append_sstr_long(&cmdstr, start);
        redis_cmd_append_sstr_long(&cmdstr, end);
    }

    /* Finally, BIT or BYTE if we were passed that argument */
    REDIS_CMD_APPEND_SSTR_OPT_STATIC(&cmdstr, !!bybit, "BIT");

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

/* BITOP */
int redis_bitop_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                    char **cmd, int *cmd_len, short *slot, void **ctx)
{
    zval *z_args;
    int i, argc = ZEND_NUM_ARGS();
    smart_string cmdstr = {0};
    short s2;

    // Allocate space for args, parse them as an array
    z_args = emalloc(argc * sizeof(zval));
    if (zend_get_parameters_array(ht, argc, z_args) == FAILURE ||
       argc < 3 || Z_TYPE(z_args[0]) != IS_STRING)
    {
        efree(z_args);
        return FAILURE;
    }

    // If we were passed a slot pointer, init to a sentinel value
    if (slot) *slot = -1;

    // Initialize command construction, add our operation argument
    redis_cmd_init_sstr(&cmdstr, argc, ZEND_STRL("BITOP"));
    redis_cmd_append_sstr(&cmdstr, Z_STRVAL(z_args[0]), Z_STRLEN(z_args[0]));

    // Now iterate over our keys argument
    for (i = 1; i < argc; i++) {
        // Append the key
        redis_cmd_append_sstr_key_zval(&cmdstr, &z_args[i], redis_sock, slot ? &s2 : NULL);

        // Verify slot if this is a Cluster request
        if (slot) {
            if (*slot != -1 && s2 != *slot) {
                php_error_docref(NULL, E_WARNING, "Warning, not all keys hash to the same slot!");
                efree(z_args);
                efree(cmdstr.c);
                return FAILURE;
            }
            *slot = s2;
        }
    }

    // Free our argument array
    efree(z_args);

    // Push out variables
    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

/* BITCOUNT */
int redis_bitcount_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                     char **cmd, int *cmd_len, short *slot, void **ctx)
{
    char *key;
    size_t key_len;
    zend_long start = 0, end = -1;
    zend_bool isbit = 0;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "s|llb", &key, &key_len,
                             &start, &end, &isbit) == FAILURE)
    {
        return FAILURE;
    }

    if (isbit) {
        *cmd_len = REDIS_CMD_SPPRINTF(cmd, "BITCOUNT", "kdds", key, key_len,
                                     (int)start, (int)end, "BIT", 3);
    } else {
        *cmd_len = REDIS_CMD_SPPRINTF(cmd, "BITCOUNT", "kdd", key, key_len,
                                     (int)start, (int)end);
    }

    return SUCCESS;
}

/* PFADD and PFMERGE are the same except that in one case we serialize,
 * and in the other case we key prefix */
static int redis_gen_pf_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                            char *kw, int kw_len, int is_keys, char **cmd,
                            int *cmd_len, short *slot)
{
    smart_string cmdstr = {0};
    zend_string *key = NULL;
    HashTable *ht = NULL;
    zval *z_ele;
    int argc=1;
    short s2;

    // Parse arguments
    ZEND_PARSE_PARAMETERS_START(2, 2)
        Z_PARAM_STR(key)
        Z_PARAM_ARRAY_HT(ht)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    argc += zend_hash_num_elements(ht);

    // We need at least two arguments
    if (argc < 2) {
        return FAILURE;
    }

    redis_cmd_init_sstr(&cmdstr, argc, kw, kw_len);
    redis_cmd_append_sstr_key_zstr(&cmdstr, key, redis_sock, slot);

    // Append our array of keys or serialized values */
    ZEND_HASH_FOREACH_VAL(ht, z_ele) {
        if (is_keys) {
            redis_cmd_append_sstr_key_zval(&cmdstr, z_ele, redis_sock, slot ? &s2 : NULL);
            if (slot && *slot != s2) {
                php_error_docref(0, E_WARNING, "All keys must hash to the same slot!");
                return FAILURE;
            }
        } else {
            redis_cmd_append_sstr_zval(&cmdstr, z_ele, redis_sock);
        }
    } ZEND_HASH_FOREACH_END();

    // Push output arguments
    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

/* PFADD */
int redis_pfadd_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                    char **cmd, int *cmd_len, short *slot, void **ctx)
{
    return redis_gen_pf_cmd(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock,
        ZEND_STRL("PFADD"), 0, cmd, cmd_len, slot);
}

/* PFMERGE */
int redis_pfmerge_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                      char **cmd, int *cmd_len, short *slot, void **ctx)
{
    return redis_gen_pf_cmd(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock,
        ZEND_STRL("PFMERGE"), 1, cmd, cmd_len, slot);
}

/* PFCOUNT */
int redis_pfcount_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                      char **cmd, int *cmd_len, short *slot, void **ctx)
{
    smart_string cmdstr = {0};
    zval *zarg = NULL, *zv;
    short slot2 = -1;
    uint32_t keys;

    ZEND_PARSE_PARAMETERS_START(1, 1)
        Z_PARAM_ZVAL(zarg)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    if (Z_TYPE_P(zarg) == IS_STRING) {
        REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 1, "PFCOUNT");
        redis_cmd_append_sstr_key_zstr(&cmdstr, Z_STR_P(zarg), redis_sock, slot);
    } else if (Z_TYPE_P(zarg) == IS_ARRAY) {
        keys = zend_hash_num_elements(Z_ARRVAL_P(zarg));
        if (keys == 0)
            return FAILURE;

        REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, keys, "PFCOUNT");

        ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(zarg), zv) {
            redis_cmd_append_sstr_key_zval(&cmdstr, zv, redis_sock, slot);
            if (slot) {
                if (slot2 != -1 && slot2 != *slot)
                    goto cross_slot;
                slot2 = *slot;
            }
        } ZEND_HASH_FOREACH_END();
    } else {
        php_error_docref(NULL, E_WARNING, "Argument must be either an array or a string");
        return FAILURE;
    }

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;
    return SUCCESS;

cross_slot:
    php_error_docref(NULL, E_WARNING, "Not all keys hash to the same slot!");
    efree(cmdstr.c);
    return FAILURE;
}

int redis_auth_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                   char **cmd, int *cmd_len, short *slot, void **ctx)
{
    zend_string *user = NULL, *pass = NULL;
    zval *ztest;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "z!", &ztest) == FAILURE ||
        redis_extract_auth_info(ztest, &user, &pass) == FAILURE)
    {
        return FAILURE;
    }

    /* Construct either AUTH <user> <pass> or AUTH <pass> */
    if (user && pass) {
        *cmd_len = REDIS_CMD_SPPRINTF(cmd, "AUTH", "SS", user, pass);
    } else {
        *cmd_len = REDIS_CMD_SPPRINTF(cmd, "AUTH", "S", pass);
    }

    redis_sock_set_auth(redis_sock, user, pass);

    if (user) zend_string_release(user);
    if (pass) zend_string_release(pass);

    return SUCCESS;
}

/* SETBIT */
int redis_setbit_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                     char **cmd, int *cmd_len, short *slot, void **ctx)
{
    char *key;
    size_t key_len;
    zend_long offset;
    zend_bool val;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "slb", &key, &key_len,
                             &offset, &val) == FAILURE)
    {
        return FAILURE;
    }

    // Validate our offset
    if (offset < BITOP_MIN_OFFSET || offset > BITOP_MAX_OFFSET) {
        php_error_docref(0, E_WARNING,
            "Invalid OFFSET for bitop command (must be between 0-2^32-1)");
        return FAILURE;
    }

    *cmd_len = REDIS_CMD_SPPRINTF(cmd, "SETBIT", "kld", key, key_len, offset, (int)val);

    return SUCCESS;
}

int redis_lmove_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                    char *kw, char **cmd, int *cmd_len, short *slot, void **ctx)
{
    zend_string *src = NULL, *dst = NULL, *from = NULL, *to = NULL;
    smart_string cmdstr = {0};
    double timeout = 0.0;
    short slot2 = 0;
    int blocking;

    blocking = toupper(*kw) == 'B';

    ZEND_PARSE_PARAMETERS_START(4 + !!blocking, 4 + !!blocking)
        Z_PARAM_STR(src)
        Z_PARAM_STR(dst)
        Z_PARAM_STR(from)
        Z_PARAM_STR(to)
        if (blocking) {
            Z_PARAM_DOUBLE(timeout)
        }
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    if (!zend_string_equals_literal_ci(from, "LEFT") && !zend_string_equals_literal_ci(from, "RIGHT")) {
        php_error_docref(NULL, E_WARNING, "Wherefrom argument must be 'LEFT' or 'RIGHT'");
        return FAILURE;
    } else if (!zend_string_equals_literal_ci(to, "LEFT") && !zend_string_equals_literal_ci(to, "RIGHT")) {
        php_error_docref(NULL, E_WARNING, "Whereto argument must be 'LEFT' or 'RIGHT'");
        return FAILURE;
    }

    redis_cmd_init_sstr(&cmdstr, 4 + !!blocking, kw, strlen(kw));
    redis_cmd_append_sstr_key_zstr(&cmdstr, src, redis_sock, slot);
    redis_cmd_append_sstr_key_zstr(&cmdstr, dst, redis_sock, slot ? &slot2 : NULL);

    /* Protect the user from CROSSLOT errors */
    if (slot && slot2 != *slot) {
        php_error_docref(NULL, E_WARNING, "Both keys must hash to the same slot!");
        efree(cmdstr.c);
        return FAILURE;
    }

    redis_cmd_append_sstr_zstr(&cmdstr, from);
    redis_cmd_append_sstr_zstr(&cmdstr, to);
    if (blocking) redis_cmd_append_sstr_dbl(&cmdstr, timeout);

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

/* LINSERT */
int redis_linsert_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                      char **cmd, int *cmd_len, short *slot, void **ctx)
{
    char *key, *pos;
    size_t key_len, pos_len;
    zval *z_val, *z_pivot;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "sszz", &key, &key_len,
                             &pos, &pos_len, &z_pivot, &z_val) == FAILURE)
    {
        return FAILURE;
    }

    // Validate position
    if (strcasecmp(pos, "after") && strcasecmp(pos, "before")) {
        php_error_docref(NULL, E_WARNING,
            "Position must be either 'BEFORE' or 'AFTER'");
        return FAILURE;
    }

    /* Construct command */
    *cmd_len = REDIS_CMD_SPPRINTF(cmd, "LINSERT", "ksvv", key, key_len, pos,
                                 pos_len, z_pivot, z_val);

    // Success
    return SUCCESS;
}

/* LREM */
int redis_lrem_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                   char **cmd, int *cmd_len, short *slot, void **ctx)
{
    char *key;
    size_t key_len;
    zend_long count = 0;
    zval *z_val;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "sz|l", &key, &key_len,
                             &z_val, &count) == FAILURE)
    {
        return FAILURE;
    }

    /* Construct command */
    *cmd_len = REDIS_CMD_SPPRINTF(cmd, "LREM", "kdv", key, key_len, count, z_val);

    // Success!
    return SUCCESS;
}

int
redis_lpos_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
               char **cmd, int *cmd_len, short *slot, void **ctx)
{
    char *key;
    int argc = 2;
    size_t key_len;
    smart_string cmdstr = {0};
    zend_bool withrank = 0;
    zend_long rank = 0, count = -1, maxlen = -1;
    zend_string *zkey;
    zval *z_val, *z_ele, *z_opts = NULL;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "sz|a",
                              &key, &key_len, &z_val, &z_opts) == FAILURE)
    {
        return FAILURE;
    }

    if (z_opts != NULL) {
        ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(z_opts), zkey, z_ele) {
            if (zkey != NULL) {
                ZVAL_DEREF(z_ele);
                if (zend_string_equals_literal_ci(zkey, "count")) {
                    count = zval_get_long(z_ele);
                } else if (zend_string_equals_literal_ci(zkey, "maxlen")) {
                    maxlen = zval_get_long(z_ele);
                } else if (zend_string_equals_literal_ci(zkey, "rank")) {
                    rank = zval_get_long(z_ele);
                    withrank = 1;
                }
            }
        } ZEND_HASH_FOREACH_END();
    }

    argc += (withrank ? 2 : 0) + (count >= 0 ? 2 : 0) + (maxlen >= 0 ? 2 : 0);
    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, argc, "LPOS");

    redis_cmd_append_sstr_key(&cmdstr, key, key_len, redis_sock, slot);
    redis_cmd_append_sstr_zval(&cmdstr, z_val, redis_sock);

    if (withrank) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "RANK");
        redis_cmd_append_sstr_long(&cmdstr, rank);
    }

    if (count >= 0) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "COUNT");
        redis_cmd_append_sstr_long(&cmdstr, count);
        *ctx = PHPREDIS_CTX_PTR;
    }

    if (maxlen >= 0) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "MAXLEN");
        redis_cmd_append_sstr_long(&cmdstr, maxlen);
    }

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

int redis_smove_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                    char **cmd, int *cmd_len, short *slot, void **ctx)
{
    zend_string *src = NULL, *dst = NULL;
    smart_string cmdstr = {0};
    zval *zv = NULL;
    short slot2;

    ZEND_PARSE_PARAMETERS_START(3, 3) {
        Z_PARAM_STR(src)
        Z_PARAM_STR(dst)
        Z_PARAM_ZVAL(zv)
    } ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 3, "SMOVE");
    redis_cmd_append_sstr_key_zstr(&cmdstr, src, redis_sock, slot);
    redis_cmd_append_sstr_key_zstr(&cmdstr, dst, redis_sock, slot ? &slot2 : NULL);
    redis_cmd_append_sstr_zval(&cmdstr, zv, redis_sock);

    if (slot && *slot != slot2) {
        php_error_docref(0, E_WARNING, "Source and destination keys don't hash to the same slot!");
        efree(cmdstr.c);
        return FAILURE;
    }

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

/* HSET */
int redis_hset_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                   char **cmd, int *cmd_len, short *slot, void **ctx)
{
    int i, argc;
    smart_string cmdstr = {0};
    zend_string *key, *zkey;
    zval *args, *z_ele;
    zend_ulong idx;

    ZEND_PARSE_PARAMETERS_START(2, -1)
        Z_PARAM_STR(key)
        Z_PARAM_VARIADIC('*', args, argc)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    if (argc == 1) {
        if (Z_TYPE_P(args) != IS_ARRAY || zend_hash_num_elements(Z_ARRVAL_P(args)) == 0) {
            return FAILURE;
        }

        /* Initialize our command */
        redis_cmd_init_sstr(&cmdstr, 1 + zend_hash_num_elements(Z_ARRVAL_P(args)) * 2, ZEND_STRL("HSET"));

        /* Append key */
        redis_cmd_append_sstr_key_zstr(&cmdstr, key, redis_sock, slot);

        ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(args), idx, zkey, z_ele) {
            ZVAL_DEREF(z_ele);
            if (zkey == NULL) {
                redis_cmd_append_sstr_long(&cmdstr, idx);
            } else {
                redis_cmd_append_sstr_zstr(&cmdstr, zkey);
            }
            redis_cmd_append_sstr_zval(&cmdstr, z_ele, redis_sock);
        } ZEND_HASH_FOREACH_END();
    } else {
        if (argc % 2 != 0) {
            return FAILURE;
        }

        /* Initialize our command */
        redis_cmd_init_sstr(&cmdstr, argc + 1, ZEND_STRL("HSET"));

        /* Append key */
        redis_cmd_append_sstr_key_zstr(&cmdstr, key, redis_sock, slot);

        for (i = 0; i < argc; ++i) {
            if (i % 2) {
                redis_cmd_append_sstr_zval(&cmdstr, &args[i], redis_sock);
            } else {
                redis_cmd_append_sstr_zval(&cmdstr, &args[i], NULL);
            }
        }
    }

    // Push out values
    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

/* HSETNX */
int redis_hsetnx_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                     char **cmd, int *cmd_len, short *slot, void **ctx)
{
    char *key, *mem;
    size_t key_len, mem_len;
    zval *z_val;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "ssz", &key, &key_len,
                             &mem, &mem_len, &z_val) == FAILURE)
    {
        return FAILURE;
    }

    /* Construct command */
    *cmd_len = REDIS_CMD_SPPRINTF(cmd, "HSETNX", "ksv", key, key_len, mem, mem_len, z_val);

    // Success
    return SUCCESS;
}

int
redis_hrandfield_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                      char **cmd, int *cmd_len, short *slot, void **ctx)
{
    char *key;
    int count = 0;
    size_t key_len;
    smart_string cmdstr = {0};
    zend_bool withvalues = 0;
    zval *z_opts = NULL, *z_ele;
    zend_string *zkey;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "s|a",
                              &key, &key_len, &z_opts) == FAILURE)
    {
        return FAILURE;
    }

    if (z_opts != NULL) {
        ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(z_opts), zkey, z_ele) {
            if (zkey != NULL) {
                ZVAL_DEREF(z_ele);
                if (zend_string_equals_literal_ci(zkey, "count")) {
                    count = zval_get_long(z_ele);
                } else if (zend_string_equals_literal_ci(zkey, "withvalues")) {
                    withvalues = zval_is_true(z_ele);
                }
            } else if (Z_TYPE_P(z_ele) == IS_STRING) {
                if (zend_string_equals_literal_ci(Z_STR_P(z_ele), "WITHVALUES")) {
                    withvalues = 1;
                }
            }
        } ZEND_HASH_FOREACH_END();
    }

    /* If we're sending WITHVALUES we must also send a count */
    if (count == 0 && withvalues)
        count = 1;

    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 1 + (count != 0) + withvalues, "HRANDFIELD");
    redis_cmd_append_sstr_key(&cmdstr, key, key_len, redis_sock, slot);

    if (count != 0) {
        redis_cmd_append_sstr_long(&cmdstr, count);
        *ctx = PHPREDIS_CTX_PTR;
    }

    if (withvalues) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "WITHVALUES");
        *ctx = PHPREDIS_CTX_PTR + 1;
    }


    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;
    return SUCCESS;
}

int redis_select_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                     char **cmd, int *cmd_len, short *slot, void **ctx)
{
    zend_long db = 0;

    ZEND_PARSE_PARAMETERS_START(1, 1)
        Z_PARAM_LONG(db)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    if (db < 0 || db > INT_MAX)
        return FAILURE;

    *ctx = (void*)(uintptr_t)db;
    *cmd_len = REDIS_CMD_SPPRINTF(cmd, "SELECT", "d", db);

    return SUCCESS;
}

/* SRANDMEMBER */
int redis_randmember_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                         char *kw, char **cmd, int *cmd_len, short *slot,
                         void **ctx)
{
    uint32_t argc = ZEND_NUM_ARGS();
    smart_string cmdstr = {0};
    zend_string *key = NULL;
    zend_long count = 0;

    ZEND_PARSE_PARAMETERS_START(1, 2)
        Z_PARAM_STR(key)
        Z_PARAM_OPTIONAL
        Z_PARAM_LONG(count)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    redis_cmd_init_sstr(&cmdstr, 1 + (argc == 2), kw, strlen(kw));
    redis_cmd_append_sstr_key_zstr(&cmdstr, key, redis_sock, slot);
    if (argc == 2)
        redis_cmd_append_sstr_long(&cmdstr, count);

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;
    *ctx = argc == 2 ? PHPREDIS_CTX_PTR : NULL;

    return SUCCESS;
}

/* ZINCRBY */
int redis_zincrby_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                      char **cmd, int *cmd_len, short *slot, void **ctx)
{
    char *key;
    size_t key_len;
    double incrby;
    zval *z_val;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "sdz", &key, &key_len,
                             &incrby, &z_val) == FAILURE)
    {
        return FAILURE;
    }

    *cmd_len = REDIS_CMD_SPPRINTF(cmd, "ZINCRBY", "kfv", key, key_len, incrby, z_val);

    return SUCCESS;
}

/* SORT */
int redis_sort_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                   char *kw, char **cmd, int *cmd_len, short *slot, void **ctx)
{
    zval *z_opts=NULL, *z_ele, z_argv;
    char *key;
    HashTable *ht_opts;
    smart_string cmdstr = {0};
    size_t key_len;
    int key_free;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "s|a", &key, &key_len,
                             &z_opts) == FAILURE)
    {
        return FAILURE;
    }

    // If we don't have an options array, the command is quite simple
    if (!z_opts || zend_hash_num_elements(Z_ARRVAL_P(z_opts)) == 0) {
        // Construct command
        *cmd_len = REDIS_CMD_SPPRINTF(cmd, kw, "k", key, key_len);

        return SUCCESS;
    }

    // Create our hash table to hold our sort arguments
    array_init(&z_argv);

    // SORT <key>
    key_free = redis_key_prefix(redis_sock, &key, &key_len);
    add_next_index_stringl(&z_argv, key, key_len);
    if (key_free) efree(key);

    // Set slot
    CMD_SET_SLOT(slot,key,key_len);

    // Grab the hash table
    ht_opts = Z_ARRVAL_P(z_opts);

    // Handle BY pattern
    if (((z_ele = zend_hash_str_find(ht_opts, "by", sizeof("by") - 1)) != NULL ||
         (z_ele = zend_hash_str_find(ht_opts, "BY", sizeof("BY") - 1)) != NULL
        ) && Z_TYPE_P(z_ele) == IS_STRING
    ) {
        // "BY" option is disabled in cluster
        if (slot) {
            php_error_docref(NULL, E_WARNING,
                "SORT BY option is not allowed in Redis Cluster");
            zval_dtor(&z_argv);
            return FAILURE;
        }

        // ... BY <pattern>
        add_next_index_stringl(&z_argv, "BY", sizeof("BY") - 1);
        add_next_index_stringl(&z_argv, Z_STRVAL_P(z_ele), Z_STRLEN_P(z_ele));
    }

    // Handle ASC/DESC option
    if (((z_ele = zend_hash_str_find(ht_opts, "sort", sizeof("sort") - 1)) != NULL ||
         (z_ele = zend_hash_str_find(ht_opts, "SORT", sizeof("SORT") - 1)) != NULL
        ) && Z_TYPE_P(z_ele) == IS_STRING
    ) {
        // 'asc'|'desc'
        add_next_index_stringl(&z_argv, Z_STRVAL_P(z_ele), Z_STRLEN_P(z_ele));
    }

    // STORE option
    if (((z_ele = zend_hash_str_find(ht_opts, "store", sizeof("store") - 1)) != NULL ||
         (z_ele = zend_hash_str_find(ht_opts, "STORE", sizeof("STORE") - 1)) != NULL
        ) && Z_TYPE_P(z_ele) == IS_STRING
    ) {
        // Slot verification
        int cross_slot = slot && *slot != cluster_hash_key(
            Z_STRVAL_P(z_ele), Z_STRLEN_P(z_ele));

        if (cross_slot) {
            php_error_docref(0, E_WARNING,
                "Error, SORT key and STORE key have different slots!");
            zval_dtor(&z_argv);
            return FAILURE;
        }

        // STORE <key>
        add_next_index_stringl(&z_argv, "STORE", sizeof("STORE") - 1);
        add_next_index_stringl(&z_argv, Z_STRVAL_P(z_ele), Z_STRLEN_P(z_ele));

        // We are using STORE
        *ctx = PHPREDIS_CTX_PTR;
    }

    // GET option
    if (((z_ele = zend_hash_str_find(ht_opts, "get", sizeof("get") - 1)) != NULL ||
         (z_ele = zend_hash_str_find(ht_opts, "GET", sizeof("GET") - 1)) != NULL
        ) && (Z_TYPE_P(z_ele) == IS_STRING || Z_TYPE_P(z_ele) == IS_ARRAY)
    ) {
        // Disabled in cluster
        if (slot) {
            php_error_docref(NULL, E_WARNING,
                "GET option for SORT disabled in Redis Cluster");
            zval_dtor(&z_argv);
            return FAILURE;
        }

        // If it's a string just add it
        if (Z_TYPE_P(z_ele) == IS_STRING) {
            add_next_index_stringl(&z_argv, "GET", sizeof("GET") - 1);
            add_next_index_stringl(&z_argv, Z_STRVAL_P(z_ele), Z_STRLEN_P(z_ele));
        } else {
            int added = 0;
            zval *z_key;

            ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(z_ele), z_key) {
                // If we can't get the data, or it's not a string, skip
                if (z_key == NULL || Z_TYPE_P(z_key) != IS_STRING) {
                    continue;
                }
                /* Add get per thing we're getting */
                add_next_index_stringl(&z_argv, "GET", sizeof("GET") - 1);

                // Add this key to our argv array
                add_next_index_stringl(&z_argv, Z_STRVAL_P(z_key), Z_STRLEN_P(z_key));
                added++;
            } ZEND_HASH_FOREACH_END();

            // Make sure we were able to add at least one
            if (added == 0) {
                php_error_docref(NULL, E_WARNING,
                    "Array of GET values requested, but none are valid");
                zval_dtor(&z_argv);
                return FAILURE;
            }
        }
    }

    // ALPHA
    if (((z_ele = zend_hash_str_find(ht_opts, "alpha", sizeof("alpha") - 1)) != NULL ||
         (z_ele = zend_hash_str_find(ht_opts, "ALPHA", sizeof("ALPHA") - 1)) != NULL) &&
         zval_is_true(z_ele)
    ) {
        add_next_index_stringl(&z_argv, "ALPHA", sizeof("ALPHA") - 1);
    }

    // LIMIT <offset> <count>
    if (((z_ele = zend_hash_str_find(ht_opts, "limit", sizeof("limit") - 1)) != NULL ||
         (z_ele = zend_hash_str_find(ht_opts, "LIMIT", sizeof("LIMIT") - 1)) != NULL
        ) && Z_TYPE_P(z_ele) == IS_ARRAY
    ) {
        HashTable *ht_off = Z_ARRVAL_P(z_ele);
        zval *z_off, *z_cnt;

        if ((z_off = zend_hash_index_find(ht_off, 0)) != NULL &&
            (z_cnt = zend_hash_index_find(ht_off, 1)) != NULL
        ) {
            if ((Z_TYPE_P(z_off) != IS_STRING && Z_TYPE_P(z_off) != IS_LONG) ||
                (Z_TYPE_P(z_cnt) != IS_STRING && Z_TYPE_P(z_cnt) != IS_LONG)
            ) {
                php_error_docref(NULL, E_WARNING,
                    "LIMIT options on SORT command must be longs or strings");
                zval_dtor(&z_argv);
                return FAILURE;
            }

            // Add LIMIT argument
            add_next_index_stringl(&z_argv, "LIMIT", sizeof("LIMIT") - 1);

            long low, high;
            if (Z_TYPE_P(z_off) == IS_STRING) {
                low = atol(Z_STRVAL_P(z_off));
            } else {
                low = Z_LVAL_P(z_off);
            }
            if (Z_TYPE_P(z_cnt) == IS_STRING) {
                high = atol(Z_STRVAL_P(z_cnt));
            } else {
                high = Z_LVAL_P(z_cnt);
            }

            // Add our two LIMIT arguments
            add_next_index_long(&z_argv, low);
            add_next_index_long(&z_argv, high);
        }
    }

    // Start constructing our command
    HashTable *ht_argv = Z_ARRVAL_P(&z_argv);
    redis_cmd_init_sstr(&cmdstr, zend_hash_num_elements(ht_argv), kw, strlen(kw));

    // Iterate through our arguments
    ZEND_HASH_FOREACH_VAL(ht_argv, z_ele) {
        // Args are strings or longs
        if (Z_TYPE_P(z_ele) == IS_STRING) {
            redis_cmd_append_sstr(&cmdstr,Z_STRVAL_P(z_ele),
                Z_STRLEN_P(z_ele));
        } else {
            redis_cmd_append_sstr_long(&cmdstr, Z_LVAL_P(z_ele));
        }
    } ZEND_HASH_FOREACH_END();

    /* Clean up our arguments array.  Note we don't have to free any prefixed
     * key as that we didn't duplicate the pointer if we prefixed */
    zval_dtor(&z_argv);

    // Push our length and command
    *cmd_len = cmdstr.len;
    *cmd     = cmdstr.c;

    // Success!
    return SUCCESS;
}

/* HDEL */
int redis_hdel_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                   char **cmd, int *cmd_len, short *slot, void **ctx)
{
    smart_string cmdstr = {0};
    zend_string *key = NULL;
    int i;
    int argc = 0;
    zval *args;

    ZEND_PARSE_PARAMETERS_START(2, -1)
        Z_PARAM_STR(key)
        Z_PARAM_VARIADIC('*', args, argc)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    // Start command construction
    redis_cmd_init_sstr(&cmdstr, argc + 1, ZEND_STRL("HDEL"));

    // Append key
    redis_cmd_append_sstr_key_zstr(&cmdstr, key, redis_sock, slot);

    // Iterate through the members we're removing
    for (i = 0; i < argc; i++) {
        redis_cmd_append_sstr_zval(&cmdstr, &args[i], NULL);
    }

    // Push out values
    *cmd     = cmdstr.c;
    *cmd_len = cmdstr.len;

    // Success!
    return SUCCESS;
}

/* ZADD */
int redis_zadd_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                   char **cmd, int *cmd_len, short *slot, void **ctx)
{
    zend_string *zstr, *key = NULL, *exp_type = NULL, *range_type = NULL;
    zend_bool ch = 0, incr = 0;
    smart_string cmdstr = {0};
    zval *argv = NULL, *z_opt;
    int argc = 0, pos = 0;

    ZEND_PARSE_PARAMETERS_START(3, -1)
        Z_PARAM_STR(key)
        Z_PARAM_VARIADIC('*', argv, argc)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    // Need key, [NX|XX] [LT|GT] [CH] [INCR] score, value, [score, value...] */
    if (argc % 2 != 0) {
        if (Z_TYPE(argv[0]) != IS_ARRAY) {
            return FAILURE;
        }

        ZEND_HASH_FOREACH_VAL(Z_ARRVAL(argv[0]), z_opt) {
            if (Z_TYPE_P(z_opt) == IS_STRING) {
                zstr = Z_STR_P(z_opt);
                if (zend_string_equals_literal_ci(zstr, "NX") || zend_string_equals_literal_ci(zstr, "XX")) {
                    exp_type = Z_STR_P(z_opt);
                } else if (zend_string_equals_literal_ci(zstr, "LT") || zend_string_equals_literal_ci(zstr, "GT")) {
                    range_type = Z_STR_P(z_opt);
                } else if (zend_string_equals_literal_ci(zstr, "CH")) {
                    ch = 1;
                } else if (zend_string_equals_literal_ci(zstr, "INCR")) {
                    if (argc != 3) {
                        // Only one score-element pair can be specified in this mode.
                        return FAILURE;
                    }
                    incr = 1;
                }
            }
        } ZEND_HASH_FOREACH_END();

        pos++;
    }

    // Start command construction
    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 1 + (argc - pos) + !!exp_type + !!range_type + !!ch + !!incr, "ZADD");
    redis_cmd_append_sstr_key_zstr(&cmdstr, key, redis_sock, slot);

    if (exp_type) redis_cmd_append_sstr_zstr(&cmdstr, exp_type);
    if (range_type) redis_cmd_append_sstr_zstr(&cmdstr, range_type);
    REDIS_CMD_APPEND_SSTR_OPT_STATIC(&cmdstr, ch, "CH");
    REDIS_CMD_APPEND_SSTR_OPT_STATIC(&cmdstr, incr, "INCR");

    // Now the rest of our arguments
    while (pos < argc) {
        // Append score and member
        if (redis_cmd_append_sstr_score(&cmdstr, &argv[pos]) == FAILURE) {
            smart_string_free(&cmdstr);
            return FAILURE;
        }

        redis_cmd_append_sstr_zval(&cmdstr, &argv[pos+1], redis_sock);

        pos += 2;
    }

    // Push output values
    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;
    *ctx = incr ? PHPREDIS_CTX_PTR : NULL;

    return SUCCESS;
}

/* OBJECT */
int redis_object_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                     char **cmd, int *cmd_len, short *slot, void **ctx)
{
    zend_string *subcmd = NULL, *key = NULL;
    smart_string cmdstr = {0};

    ZEND_PARSE_PARAMETERS_START(2, 2)
        Z_PARAM_STR(subcmd)
        Z_PARAM_STR(key)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    if (zend_string_equals_literal_ci(subcmd, "REFCOUNT") ||
        zend_string_equals_literal_ci(subcmd, "IDLETIME"))
    {
        *ctx = PHPREDIS_CTX_PTR;
    } else if (zend_string_equals_literal_ci(subcmd, "ENCODING")) {
        *ctx = PHPREDIS_CTX_PTR + 1;
    } else {
        php_error_docref(NULL, E_WARNING, "Invalid subcommand sent to OBJECT");
        return FAILURE;
    }

    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 2, "OBJECT");
    redis_cmd_append_sstr_zstr(&cmdstr, subcmd);
    redis_cmd_append_sstr_key_zstr(&cmdstr, key, redis_sock, slot);

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;
    return SUCCESS;
}

int
redis_geoadd_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                 char **cmd, int *cmd_len, short *slot, void **ctx)
{
    zval *z_args, *z_ele;
    smart_string cmdstr = {0};
    zend_bool ch = 0;
    zend_string *zstr;
    char *mode = NULL;
    int argc, i;

    // We at least need a key and three values
    if ((argc = ZEND_NUM_ARGS()) < 4 || (argc % 3 != 1 && argc % 3 != 2)) {
        zend_wrong_param_count();
        return FAILURE;
    }

    // Make sure we at least have a key, and we can get other args
    z_args = ecalloc(argc, sizeof(*z_args));
    if (zend_get_parameters_array(ht, argc, z_args) == FAILURE) {
        efree(z_args);
        return FAILURE;
    }

    if (argc % 3 == 2) {
        argc--;
        if (Z_TYPE(z_args[argc]) != IS_ARRAY) {
            php_error_docref(NULL, E_WARNING, "Invalid options value");
            efree(z_args);
            return FAILURE;
        }
        ZEND_HASH_FOREACH_VAL(Z_ARRVAL(z_args[argc]), z_ele) {
            ZVAL_DEREF(z_ele);
            if (Z_TYPE_P(z_ele) == IS_STRING) {
                if (zend_string_equals_literal_ci(Z_STR_P(z_ele), "NX") ||
                    zend_string_equals_literal_ci(Z_STR_P(z_ele), "XX"))
                {
                    mode = Z_STRVAL_P(z_ele);
                } else if (zend_string_equals_literal_ci(Z_STR_P(z_ele), "CH")) {
                    ch = 1;
                }
            }
        } ZEND_HASH_FOREACH_END();
    }

    /* Initialize our command */
    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, argc + (mode != NULL) + ch, "GEOADD");

    /* Append key */
    zstr = zval_get_string(&z_args[0]);
    redis_cmd_append_sstr_key(&cmdstr, ZSTR_VAL(zstr), ZSTR_LEN(zstr), redis_sock, slot);
    zend_string_release(zstr);

    /* Append options */
    if (mode != NULL) {
        redis_cmd_append_sstr(&cmdstr, mode, strlen(mode));
    }
    REDIS_CMD_APPEND_SSTR_OPT_STATIC(&cmdstr, ch, "CH");

    /* Append members */
    for (i = 1; i < argc; ++i) {
        redis_cmd_append_sstr_zval(&cmdstr, &z_args[i], redis_sock);
    }

    // Cleanup arg array
    efree(z_args);

    // Push out values
    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

/* GEODIST */
int redis_geodist_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                      char **cmd, int *cmd_len, short *slot, void **ctx)
{
    char *key, *source, *dest, *unit = NULL;
    size_t keylen, sourcelen, destlen, unitlen;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "sss|s", &key, &keylen,
                              &source, &sourcelen, &dest, &destlen, &unit,
                              &unitlen) == FAILURE)
    {
        return FAILURE;
    }

    /* Construct command */
    if (unit != NULL) {
        *cmd_len = REDIS_CMD_SPPRINTF(cmd, "GEODIST", "ksss", key, keylen, source,
                                     sourcelen, dest, destlen, unit, unitlen);
    } else {
        *cmd_len = REDIS_CMD_SPPRINTF(cmd, "GEODIST", "kss", key, keylen, source,
                                     sourcelen, dest, destlen);
    }

    return SUCCESS;
}

geoStoreType get_georadius_store_type(zend_string *key) {
    if (ZSTR_LEN(key) == 5 && !strcasecmp(ZSTR_VAL(key), "store")) {
        return STORE_COORD;
    } else if (ZSTR_LEN(key) == 9 && !strcasecmp(ZSTR_VAL(key), "storedist")) {
        return STORE_DIST;
    }

    return STORE_NONE;
}

/* Helper function to get COUNT and possible ANY flag which is passable to
 * both GEORADIUS and GEOSEARCH */
static int get_georadius_count_options(zval *optval, geoOptions *opts) {
    zval *z_tmp;

    /* Short circuit on bad options */
    if (Z_TYPE_P(optval) != IS_ARRAY && Z_TYPE_P(optval) != IS_LONG)
        goto error;

    if (Z_TYPE_P(optval) == IS_ARRAY) {
        z_tmp = zend_hash_index_find(Z_ARRVAL_P(optval), 0);
        if (z_tmp) {
            if (Z_TYPE_P(z_tmp) != IS_LONG || Z_LVAL_P(z_tmp) <= 0)
                goto error;
            opts->count = Z_LVAL_P(z_tmp);
        }

        z_tmp = zend_hash_index_find(Z_ARRVAL_P(optval), 1);
        if (z_tmp) {
            opts->any = zval_is_true(z_tmp);
        }
    } else {
        if (Z_LVAL_P(optval) <= 0)
            goto error;
        opts->count = Z_LVAL_P(optval);
    }

    return SUCCESS;

error:
    php_error_docref(NULL, E_WARNING, "Invalid COUNT value");
    return FAILURE;
}

/* Helper function to extract optional arguments for GEORADIUS and GEORADIUSBYMEMBER */
static int get_georadius_opts(HashTable *ht, geoOptions *opts) {
    zend_string *zkey;
    char *optstr;
    zval *optval;

    /* Iterate over our argument array, collating which ones we have */
    ZEND_HASH_FOREACH_STR_KEY_VAL(ht, zkey, optval) {
        ZVAL_DEREF(optval);

        /* If the key is numeric it's a non value option */
        if (zkey) {
            if (zend_string_equals_literal_ci(zkey, "COUNT")) {
                if (get_georadius_count_options(optval, opts) == FAILURE) {
                    if (opts->key) zend_string_release(opts->key);
                    return FAILURE;
                }
            } else if (opts->store == STORE_NONE) {
                opts->store = get_georadius_store_type(zkey);
                if (opts->store != STORE_NONE) {
                    opts->key = zval_get_string(optval);
                }
            }
        } else {
            /* Option needs to be a string */
            if (Z_TYPE_P(optval) != IS_STRING) continue;

            optstr = Z_STRVAL_P(optval);

            if (!strcasecmp(optstr, "withcoord")) {
                opts->withcoord = 1;
            } else if (!strcasecmp(optstr, "withdist")) {
                opts->withdist = 1;
            } else if (!strcasecmp(optstr, "withhash")) {
                opts->withhash = 1;
            } else if (!strcasecmp(optstr, "asc")) {
                opts->sort = SORT_ASC;
            } else if (!strcasecmp(optstr, "desc")) {
                opts->sort = SORT_DESC;
            }
        }
    } ZEND_HASH_FOREACH_END();

    /* STORE and STOREDIST are not compatible with the WITH* options */
    if (opts->key != NULL && (opts->withcoord || opts->withdist || opts->withhash)) {
        php_error_docref(NULL, E_WARNING,
            "STORE[DIST] is not compatible with WITHCOORD, WITHDIST or WITHHASH");

        if (opts->key) zend_string_release(opts->key);
        return FAILURE;
    }

    /* Success */
    return SUCCESS;
}

/* Helper to append options to a GEORADIUS or GEORADIUSBYMEMBER command */
void append_georadius_opts(RedisSock *redis_sock, smart_string *str, short *slot,
                           geoOptions *opt)
{
    if (opt->withcoord)
        REDIS_CMD_APPEND_SSTR_STATIC(str, "WITHCOORD");
    if (opt->withdist)
        REDIS_CMD_APPEND_SSTR_STATIC(str, "WITHDIST");
    if (opt->withhash)
        REDIS_CMD_APPEND_SSTR_STATIC(str, "WITHHASH");

    /* Append sort if it's not GEO_NONE */
    if (opt->sort == SORT_ASC) {
        REDIS_CMD_APPEND_SSTR_STATIC(str, "ASC");
    } else if (opt->sort == SORT_DESC) {
        REDIS_CMD_APPEND_SSTR_STATIC(str, "DESC");
    }

    /* Append our count if we've got one */
    if (opt->count) {
        REDIS_CMD_APPEND_SSTR_STATIC(str, "COUNT");
        redis_cmd_append_sstr_long(str, opt->count);
        if (opt->any) {
            REDIS_CMD_APPEND_SSTR_STATIC(str, "ANY");
        }
    }

    /* Append store options if we've got them */
    if (opt->store != STORE_NONE && opt->key != NULL) {
        if (opt->store == STORE_COORD) {
            REDIS_CMD_APPEND_SSTR_STATIC(str, "STORE");
        } else {
            REDIS_CMD_APPEND_SSTR_STATIC(str, "STOREDIST");
        }

        redis_cmd_append_sstr_key_zstr(str, opt->key, redis_sock, slot);
    }
}

/* GEORADIUS / GEORADIUS_RO */
int redis_georadius_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                        char *kw, char **cmd, int *cmd_len, short *slot,
                        void **ctx)
{
    zend_string *key = NULL, *unit = NULL;
    double lng = 0, lat = 0, radius = 0;
    smart_string cmdstr = {0};
    HashTable *opts = NULL;
    geoOptions gopts = {0};
    short store_slot = -1;
    uint32_t argc;

    ZEND_PARSE_PARAMETERS_START(5, 6)
        Z_PARAM_STR(key)
        Z_PARAM_DOUBLE(lng)
        Z_PARAM_DOUBLE(lat)
        Z_PARAM_DOUBLE(radius)
        Z_PARAM_STR(unit)
        Z_PARAM_OPTIONAL
        Z_PARAM_ARRAY_HT_OR_NULL(opts)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    /* Parse any GEORADIUS options we have */
    if (opts != NULL && get_georadius_opts(opts, &gopts) != SUCCESS)
        return FAILURE;

    /* Increment argc depending on options */
    argc = 5 + gopts.withcoord + gopts.withdist + gopts.withhash +
               (gopts.sort != SORT_NONE) + (gopts.count ? 2 + gopts.any : 0) +
               (gopts.store != STORE_NONE ? 2 : 0);

    /* Begin construction of our command */
    redis_cmd_init_sstr(&cmdstr, argc, kw, strlen(kw));
    redis_cmd_append_sstr_key_zstr(&cmdstr, key, redis_sock, slot);

    /* Append required arguments */
    redis_cmd_append_sstr_dbl(&cmdstr, lng);
    redis_cmd_append_sstr_dbl(&cmdstr, lat);
    redis_cmd_append_sstr_dbl(&cmdstr, radius);
    redis_cmd_append_sstr_zstr(&cmdstr, unit);

    /* Append optional arguments */
    append_georadius_opts(redis_sock, &cmdstr, slot ? &store_slot : NULL, &gopts);

    /* Free key if it was prefixed */
    if (gopts.key) zend_string_release(gopts.key);

    /* Protect the user from CROSSSLOT if we're in cluster */
    if (slot && gopts.store != STORE_NONE && *slot != store_slot) {
        php_error_docref(NULL, E_WARNING,
            "Key and STORE[DIST] key must hash to the same slot");
        efree(cmdstr.c);
        return FAILURE;
    }

    /* Set slot, command and len, and return */
    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

/* GEORADIUSBYMEMBER/GEORADIUSBYMEMBER_RO
 *    key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] */
int redis_georadiusbymember_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                                char *kw, char **cmd, int *cmd_len, short *slot,
                                void **ctx)
{
    char *key, *mem, *unit;
    size_t keylen, memlen, unitlen;
    short store_slot = 0;
    int keyfree, argc = 4;
    double radius;
    geoOptions gopts = {0};
    zval *opts = NULL;
    smart_string cmdstr = {0};

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "ssds|a", &key, &keylen,
                              &mem, &memlen, &radius, &unit, &unitlen, &opts) == FAILURE)
    {
        return FAILURE;
    }

    if (opts != NULL) {
        /* Attempt to parse our options array */
        if (get_georadius_opts(Z_ARRVAL_P(opts), &gopts) == FAILURE) {
            return FAILURE;
        }
    }

    /* Increment argc based on options */
    argc += gopts.withcoord + gopts.withdist + gopts.withhash +
            (gopts.sort != SORT_NONE) + (gopts.count ? 2 + gopts.any : 0) +
            (gopts.store != STORE_NONE ? 2 : 0);

    /* Begin command construction*/
    redis_cmd_init_sstr(&cmdstr, argc, kw, strlen(kw));

    /* Prefix our key if we're prefixing and set the slot */
    keyfree = redis_key_prefix(redis_sock, &key, &keylen);
    CMD_SET_SLOT(slot, key, keylen);

    /* Append required arguments */
    redis_cmd_append_sstr(&cmdstr, key, keylen);
    redis_cmd_append_sstr(&cmdstr, mem, memlen);
    redis_cmd_append_sstr_long(&cmdstr, radius);
    redis_cmd_append_sstr(&cmdstr, unit, unitlen);

    /* Append options */
    append_georadius_opts(redis_sock, &cmdstr, slot ? &store_slot : NULL, &gopts);

    /* Free key if we prefixed */
    if (keyfree) efree(key);
    if (gopts.key) zend_string_release(gopts.key);

    /* Protect the user from CROSSSLOT if we're in cluster */
    if (slot && gopts.store != STORE_NONE && *slot != store_slot) {
        php_error_docref(NULL, E_WARNING,
            "Key and STORE[DIST] key must hash to the same slot");
        efree(cmdstr.c);
        return FAILURE;
    }

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

int
redis_geosearch_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                    char **cmd, int *cmd_len, short *slot, void **ctx)
{
    char *key, *unit;
    int argc = 0;
    size_t keylen, unitlen;
    geoOptions gopts = {0};
    smart_string cmdstr = {0};
    zval *position, *shape, *opts = NULL, *z_ele;
    zend_string *zkey, *zstr;
    zend_bool bypolygon = 0;
    HashTable *ht;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "szzs|a", &key, &keylen,
                              &position, &shape, &unit, &unitlen,
                              &opts) == FAILURE)
    {
        return FAILURE;
    }

    if (Z_TYPE_P(shape) == IS_LONG || Z_TYPE_P(shape) == IS_DOUBLE) {
        argc += 2;
    } else if (Z_TYPE_P(shape) == IS_ARRAY &&
               zend_hash_num_elements(Z_ARRVAL_P(shape)) == 2)
    {
        // BYBOX
        argc += 3;
    } else if (Z_TYPE_P(shape) == IS_ARRAY &&
               zend_hash_num_elements(Z_ARRVAL_P(shape)) >= 6 &&
               zend_hash_num_elements(Z_ARRVAL_P(shape)) % 2 == 0)
    {
        // BYPOLYGON N verticies
        argc += 2 + zend_hash_num_elements(Z_ARRVAL_P(shape));
        bypolygon = 1;
    } else {
        php_error_docref(NULL, E_WARNING, "Invalid shape dimensions");
        return FAILURE;
    }

    if (!bypolygon) {
        if (Z_TYPE_P(position) == IS_STRING && Z_STRLEN_P(position) > 0) {
            argc += 2;
        } else if (Z_TYPE_P(position) == IS_ARRAY &&
                   zend_hash_num_elements(Z_ARRVAL_P(position)) == 2)
        {
            argc += 3;
        } else {
            php_error_docref(NULL, E_WARNING, "Invalid position");
            return FAILURE;
        }
    }

    argc += bypolygon ? 1 : 2;

    /* Attempt to parse our options array */
    if (opts != NULL) {
        ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(opts), zkey, z_ele) {
            ZVAL_DEREF(z_ele);
            if (zkey != NULL && zend_string_equals_literal_ci(zkey, "COUNT")) {
                if (get_georadius_count_options(z_ele, &gopts) == FAILURE) {
                    return FAILURE;
                }
            } else if (Z_TYPE_P(z_ele) == IS_STRING) {
                zstr = Z_STR_P(z_ele);
                if (zend_string_equals_literal_ci(zstr, "WITHCOORD")) {
                    gopts.withcoord = 1;
                } else if (zend_string_equals_literal_ci(zstr, "WITHDIST")) {
                    gopts.withdist = 1;
                } else if (zend_string_equals_literal_ci(zstr, "WITHHASH")) {
                    gopts.withhash = 1;
                } else if (zend_string_equals_literal_ci(zstr, "ASC")) {
                    gopts.sort = SORT_ASC;
                } else if (zend_string_equals_literal_ci(zstr, "DESC")) {
                    gopts.sort = SORT_DESC;
                }
            }
        } ZEND_HASH_FOREACH_END();
    }

    /* Increment argc based on options */
    argc += gopts.withcoord + gopts.withdist + gopts.withhash
         + (gopts.sort != SORT_NONE) + (gopts.count ? 2 + gopts.any : 0);

    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, argc, "GEOSEARCH");
    redis_cmd_append_sstr_key(&cmdstr, key, keylen, redis_sock, slot);

    if (!bypolygon) {
        if (Z_TYPE_P(position) == IS_ARRAY) {
            REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "FROMLONLAT");
            ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(position), z_ele) {
                ZVAL_DEREF(z_ele);
                redis_cmd_append_sstr_dbl(&cmdstr, zval_get_double(z_ele));
            } ZEND_HASH_FOREACH_END();
        } else {
            REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "FROMMEMBER");
            redis_cmd_append_sstr(&cmdstr, Z_STRVAL_P(position), Z_STRLEN_P(position));
        }
    }

    if (Z_TYPE_P(shape) == IS_ARRAY) {
        ht = Z_ARRVAL_P(shape);
        if (bypolygon) {
            REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "BYPOLYGON");
            redis_cmd_append_sstr_long(&cmdstr, zend_hash_num_elements(ht) / 2);
        } else {
            REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "BYBOX");
        }

        ZEND_HASH_FOREACH_VAL(ht, z_ele) {
            ZVAL_DEREF(z_ele);
            redis_cmd_append_sstr_dbl(&cmdstr, zval_get_double(z_ele));
        } ZEND_HASH_FOREACH_END();
    } else {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "BYRADIUS");
        redis_cmd_append_sstr_dbl(&cmdstr, zval_get_double(shape));
    }

    if (!bypolygon) {
        redis_cmd_append_sstr(&cmdstr, unit, unitlen);
    }

    /* Append optional arguments */
    if (gopts.withcoord) REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "WITHCOORD");
    if (gopts.withdist) REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "WITHDIST");
    if (gopts.withhash) REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "WITHHASH");

    /* Append sort if it's not GEO_NONE */
    if (gopts.sort == SORT_ASC) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "ASC");
    } else if (gopts.sort == SORT_DESC) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "DESC");
    }

    /* Append our count if we've got one */
    if (gopts.count) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "COUNT");
        redis_cmd_append_sstr_long(&cmdstr, gopts.count);
        if (gopts.any) {
            REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "ANY");
        }
    }

    if (gopts.withcoord + gopts.withdist + gopts.withhash > 0) {
        *ctx = PHPREDIS_CTX_PTR;
    }

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

int
redis_geosearchstore_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                         char **cmd, int *cmd_len, short *slot, void **ctx)
{
    char *dest, *src, *unit;
    size_t destlen, srclen, unitlen;
    geoOptions gopts = {0};
    smart_string cmdstr = {0};
    zval *position, *shape, *opts = NULL, *z_ele;
    zend_bool bypolygon = 0;
    zend_string *zkey;
    HashTable *ht;
    short s2 = 0;
    int argc = 0;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "sszzs|a",
                              &dest, &destlen, &src, &srclen, &position, &shape,
                              &unit, &unitlen, &opts) == FAILURE)
    {
        return FAILURE;
    }

    if (Z_TYPE_P(shape) == IS_LONG || Z_TYPE_P(shape) == IS_DOUBLE) {
        argc += 2;
    } else if (Z_TYPE_P(shape) == IS_ARRAY &&
               zend_hash_num_elements(Z_ARRVAL_P(shape)) == 2)
    {
        argc += 3;
    } else if (Z_TYPE_P(shape) == IS_ARRAY &&
               zend_hash_num_elements(Z_ARRVAL_P(shape)) >= 6 &&
               zend_hash_num_elements(Z_ARRVAL_P(shape)) % 2 == 0)
    {
        argc += 2 + zend_hash_num_elements(Z_ARRVAL_P(shape));
        bypolygon = 1;
    } else {
        php_error_docref(NULL, E_WARNING, "Invalid shape dimensions");
        return FAILURE;
    }

    argc += bypolygon ? 2 : 3;

    if (!bypolygon) {
        if (Z_TYPE_P(position) == IS_STRING && Z_STRLEN_P(position) > 0) {
            argc += 2;
        } else if (Z_TYPE_P(position) == IS_ARRAY &&
                   zend_hash_num_elements(Z_ARRVAL_P(position)) == 2)
        {
            argc += 3;
        } else {
            php_error_docref(NULL, E_WARNING, "Invalid position");
            return FAILURE;
        }
    }

    /* Attempt to parse our options array */
    if (opts != NULL) {
        ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(opts), zkey, z_ele) {
            ZVAL_DEREF(z_ele);
            if (zkey != NULL) {
                if (zend_string_equals_literal_ci(zkey, "COUNT")) {
                    if (Z_TYPE_P(z_ele) != IS_LONG || Z_LVAL_P(z_ele) <= 0) {
                        php_error_docref(NULL, E_WARNING, "COUNT must be an integer > 0!");
                        return FAILURE;
                    }
                    gopts.count = Z_LVAL_P(z_ele);
                }
            } else if (Z_TYPE_P(z_ele) == IS_STRING) {
                if (!strcasecmp(Z_STRVAL_P(z_ele), "ASC")) {
                    gopts.sort = SORT_ASC;
                } else if (!strcasecmp(Z_STRVAL_P(z_ele), "DESC")) {
                    gopts.sort = SORT_DESC;
                } else if (!strcasecmp(Z_STRVAL_P(z_ele), "STOREDIST")) {
                    gopts.store = STORE_DIST;
                }
            }
        } ZEND_HASH_FOREACH_END();

    }

    /* Increment argc based on options */
    argc += gopts.withcoord + gopts.withdist + gopts.withhash
         + (gopts.sort != SORT_NONE) + (gopts.count ? 2 : 0)
         + (gopts.store != STORE_NONE);

    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, argc, "GEOSEARCHSTORE");
    redis_cmd_append_sstr_key(&cmdstr, dest, destlen, redis_sock, slot);
    redis_cmd_append_sstr_key(&cmdstr, src, srclen, redis_sock, slot ? &s2 : NULL);

    if (slot && *slot != s2) {
        php_error_docref(NULL, E_WARNING, "All keys must hash to the same slot");
        efree(cmdstr.c);
        return FAILURE;
    }

    if (!bypolygon) {
        if (Z_TYPE_P(position) == IS_ARRAY) {
            REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "FROMLONLAT");
            ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(position), z_ele) {
                ZVAL_DEREF(z_ele);
                redis_cmd_append_sstr_dbl(&cmdstr, zval_get_double(z_ele));
            } ZEND_HASH_FOREACH_END();
        } else {
            REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "FROMMEMBER");
            redis_cmd_append_sstr(&cmdstr, Z_STRVAL_P(position), Z_STRLEN_P(position));
        }
    }

    if (Z_TYPE_P(shape) == IS_ARRAY) {
        ht = Z_ARRVAL_P(shape);
        if (bypolygon) {
            REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "BYPOLYGON");
            redis_cmd_append_sstr_long(&cmdstr, zend_hash_num_elements(ht) / 2);
        } else {
            REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "BYBOX");
        }
        ZEND_HASH_FOREACH_VAL(ht, z_ele) {
            ZVAL_DEREF(z_ele);
            redis_cmd_append_sstr_dbl(&cmdstr, zval_get_double(z_ele));
        } ZEND_HASH_FOREACH_END();
    } else {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "BYRADIUS");
        redis_cmd_append_sstr_dbl(&cmdstr, zval_get_double(shape));
    }

    if (!bypolygon) {
        redis_cmd_append_sstr(&cmdstr, unit, unitlen);
    }

    /* Append sort if it's not GEO_NONE */
    if (gopts.sort == SORT_ASC) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "ASC");
    } else if (gopts.sort == SORT_DESC) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "DESC");
    }

    /* Append our count if we've got one */
    if (gopts.count) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "COUNT");
        redis_cmd_append_sstr_long(&cmdstr, gopts.count);
    }

    if (gopts.store == STORE_DIST) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "STOREDIST");
    }

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

/*  MIGRATE host port <key | ""> destination-db timeout [COPY] [REPLACE]
            [[AUTH password] | [AUTH2 username password]] [KEYS key [key ...]]

    Starting with Redis version 3.0.0: Added the COPY and REPLACE options.
    Starting with Redis version 3.0.6: Added the KEYS option.
    Starting with Redis version 4.0.7: Added the AUTH option.
    Starting with Redis version 6.0.0: Added the AUTH2 option.
*/

int redis_httl_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, char *kw,
                   char **cmd, int *cmd_len, short *slot, void **ctx)
{
    smart_string cmdstr = {0};
    zend_string *key, *field, *tmp;
    HashTable *fields;
    int argc;
    zval *zv;

    ZEND_PARSE_PARAMETERS_START(2, 2)
        Z_PARAM_STR(key)
        Z_PARAM_ARRAY_HT(fields)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    if (zend_hash_num_elements(fields) < 1) {
        php_error_docref(NULL, E_WARNING, "Must pass at least one field");
        return FAILURE;
    }

    // 3 because <key> FIELDS <num_fields>
    argc = 3 + zend_hash_num_elements(fields);
    redis_cmd_init_sstr(&cmdstr, argc, kw, strlen(kw));

    redis_cmd_append_sstr_key_zstr(&cmdstr, key, redis_sock, slot);
    REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "FIELDS");
    redis_cmd_append_sstr_long(&cmdstr, zend_hash_num_elements(fields));

    ZEND_HASH_FOREACH_VAL(fields, zv)
        field = zval_get_tmp_string(zv, &tmp);
        redis_cmd_append_sstr_zstr(&cmdstr, field);
        zend_tmp_string_release(tmp);
    ZEND_HASH_FOREACH_END();

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

typedef struct redisHGetExOptions {
    zend_string *exp_type;
    zend_long exp_arg;
} redisHGetExOptions;

static int get_hgetex_expiry_opts(redisHGetExOptions *dst, zval *zv) {
    zend_string *zkey;
    HashTable *ht;
    zval *zexp;

    *dst = (redisHGetExOptions) {
        .exp_type = NULL,
        .exp_arg = -1
    };

    if (zv == NULL)
        return SUCCESS;

    if (Z_TYPE_P(zv) != IS_STRING && Z_TYPE_P(zv) != IS_ARRAY) {
        php_error_docref(NULL, E_WARNING,
            "Expiry value must be a string or an array of strings");
        return FAILURE;
    }

    if (Z_TYPE_P(zv) == IS_STRING) {
        dst->exp_type = Z_STR_P(zv);
        dst->exp_arg = -1;
        return SUCCESS;
    }

    ht = Z_ARRVAL_P(zv);

    ZEND_HASH_FOREACH_STR_KEY_VAL(ht, zkey, zexp)
        if (zkey == NULL)
            continue;

        if (Z_TYPE_P(zexp) != IS_LONG || Z_LVAL_P(zexp) < 0) {
            php_error_docref(NULL, E_WARNING,
                "Expiry must be an integer >= 0");
            return FAILURE;
        }

        dst->exp_type = zkey;
        dst->exp_arg = Z_LVAL_P(zexp);
    ZEND_HASH_FOREACH_END();

    return SUCCESS;
}

int redis_hgetex_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                     char **cmd, int *cmd_len, short *slot, void **ctx)
{
    redisHGetExOptions opts = {0};
    smart_string cmdstr = {0};
    HashTable *fields, *htctx;
    zval *zexpiry = NULL;
    zend_string *key;
    int argc;

    ZEND_PARSE_PARAMETERS_START(2, 3)
        Z_PARAM_STR(key)
        Z_PARAM_ARRAY_HT(fields)
        Z_PARAM_OPTIONAL
        Z_PARAM_ZVAL_OR_NULL(zexpiry);
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    if (zend_hash_num_elements(fields) == 0) {
        php_error_docref(NULL, E_WARNING, "Must pass at least one field");
        return FAILURE;
    } else if (get_hgetex_expiry_opts(&opts, zexpiry) == FAILURE) {
        return FAILURE;
    }

    htctx = build_hash_context_ht(fields, hmget_filter);
    if (htctx == NULL) {
        php_error_docref(NULL, E_WARNING,
            "Failed to build context hash table");
        return FAILURE;
    }

    argc = 3 + (opts.exp_type != NULL) + (opts.exp_arg >= 0) +
           zend_hash_num_elements(fields);

    redis_cmd_init_sstr(&cmdstr, argc, ZEND_STRL("HGETEX"));
    redis_cmd_append_sstr_key_zstr(&cmdstr, key, redis_sock, slot);
    if (opts.exp_type) {
        redis_cmd_append_sstr_zstr(&cmdstr, opts.exp_type);
        if (opts.exp_arg >= 0)
            redis_cmd_append_sstr_long(&cmdstr, opts.exp_arg);
    }

    REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "FIELDS");
    redis_cmd_append_sstr_long(&cmdstr, zend_hash_num_elements(htctx));
    redis_cmd_append_sstr_hash_fields(&cmdstr, htctx);

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;
    *ctx = htctx;

    return SUCCESS;
}

typedef struct redisHSetExOptions {
    zend_string *set_mode;
    zend_string *exp_type;
    zend_long exp_arg;
} redisHSetExOptions;

void get_hsetex_expiry_options(redisHSetExOptions *dst, HashTable *src) {
    zend_string *key;
    zend_long lval;
    zval *zv;

    *dst = (redisHSetExOptions) {
        .exp_arg = -1,
    };

    if (src == NULL)
        return;

    ZEND_HASH_FOREACH_STR_KEY_VAL(src, key, zv) {
        if (key == NULL) {
            if (Z_TYPE_P(zv) == IS_STRING) {
                key = Z_STR_P(zv);
                if (zend_string_equals_literal_ci(key, "FNX") ||
                    zend_string_equals_literal_ci(key, "FXX"))
                {
                    dst->set_mode = Z_STR_P(zv);
                } else if (zend_string_equals_literal_ci(key, "KEEPTTL")) {
                    dst->exp_type = key;
                    dst->exp_arg = -1;
                }
            }
        } else if (zend_string_equals_literal_ci(key, "EX") ||
                   zend_string_equals_literal_ci(key, "PX") ||
                   zend_string_equals_literal_ci(key, "EXAT") ||
                   zend_string_equals_literal_ci(key, "PXAT"))
        {
            lval = zval_get_long(zv);
            if (lval >= 0) {
                dst->exp_type = key;
                dst->exp_arg = lval;
            } else {
                php_error_docref(NULL, E_WARNING, "Timeouts must be >= 0");
            }
        }
    } ZEND_HASH_FOREACH_END();
}

int redis_hsetex_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                     char **cmd, int *cmd_len, short *slot, void **ctx)
{
    redisHSetExOptions opts = {0};
    smart_string cmdstr = {0};
    HashTable *fields, *expiry = NULL;
    zend_string *key;
    zend_ulong idx;
    zval *zv;
    int argc;

    ZEND_PARSE_PARAMETERS_START(2, 3)
        Z_PARAM_STR(key)
        Z_PARAM_ARRAY_HT(fields)
        Z_PARAM_OPTIONAL
        Z_PARAM_ARRAY_HT_OR_NULL(expiry);
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    if (zend_hash_num_elements(fields) == 0) {
        php_error_docref(NULL, E_WARNING, "Must pass at least one field");
        return FAILURE;
    }

    get_hsetex_expiry_options(&opts, expiry);

    argc = 3 + !!opts.set_mode + !!opts.exp_type + (opts.exp_arg >= 0) +
           zend_hash_num_elements(fields) * 2;

    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, argc, "HSETEX");
    redis_cmd_append_sstr_key_zstr(&cmdstr, key, redis_sock, slot);
    if (opts.set_mode)
        redis_cmd_append_sstr_zstr(&cmdstr, opts.set_mode);
    if (opts.exp_type) {
        redis_cmd_append_sstr_zstr(&cmdstr, opts.exp_type);
        if (opts.exp_arg >= 0)
            redis_cmd_append_sstr_long(&cmdstr, opts.exp_arg);
    }

    REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "FIELDS");
    redis_cmd_append_sstr_long(&cmdstr, zend_hash_num_elements(fields));

    ZEND_HASH_FOREACH_KEY_VAL(fields, idx, key, zv)
        if (key) {
            redis_cmd_append_sstr_zstr(&cmdstr, key);
        } else {
            redis_cmd_append_sstr_long(&cmdstr, idx);
        }
        redis_cmd_append_sstr_zval(&cmdstr, zv, redis_sock);
    ZEND_HASH_FOREACH_END();

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

int redis_hgetdel_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                      char **cmd, int *cmd_len, short *slot, void **ctx)
{
    HashTable *fields, *htctx;
    smart_string cmdstr = {0};
    zend_string *key;
    int argc;

    ZEND_PARSE_PARAMETERS_START(2, 2)
        Z_PARAM_STR(key);
        Z_PARAM_ARRAY_HT(fields);
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    if (zend_hash_num_elements(fields) == 0) {
        php_error_docref(NULL, E_WARNING, "Must pass at least one field");
        return FAILURE;
    }

    htctx = build_hash_context_ht(fields, hmget_filter);
    if (htctx == NULL) {
        php_error_docref(NULL, E_WARNING,
            "Failed to build context hash table");
        return FAILURE;
    }

    argc = 3 + zend_hash_num_elements(htctx);

    redis_cmd_init_sstr(&cmdstr, argc, ZEND_STRL("HGETDEL"));
    redis_cmd_append_sstr_key_zstr(&cmdstr, key, redis_sock, slot);

    REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "FIELDS");
    redis_cmd_append_sstr_long(&cmdstr, zend_hash_num_elements(htctx));
    redis_cmd_append_sstr_hash_fields(&cmdstr, htctx);

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;
    *ctx = htctx;

    return SUCCESS;
}

int redis_hexpire_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                      char *kw, char **cmd, int *cmd_len, short *slot,
                      void **ctx)
{
    zend_string *key, *option = NULL, *tmp, *field;
    smart_string cmdstr = {0};
    HashTable *fields;
    zend_long ttl;
    zval *zv;
    int argc;

    ZEND_PARSE_PARAMETERS_START(3, 4)
        Z_PARAM_STR(key)
        Z_PARAM_LONG(ttl)
        Z_PARAM_ARRAY_HT(fields)
        Z_PARAM_OPTIONAL
        Z_PARAM_STR_OR_NULL(option)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    if (zend_hash_num_elements(fields) < 1) {
        php_error_docref(NULL, E_WARNING, "Must pass at least one field");
        return FAILURE;
    }

    // 4 because <key> <ttl> FIELDS <num_fields>
    argc = 4 + zend_hash_num_elements(fields) + (option ? 1 : 0);
    redis_cmd_init_sstr(&cmdstr, argc, kw, strlen(kw));

    redis_cmd_append_sstr_key_zstr(&cmdstr, key, redis_sock, slot);
    redis_cmd_append_sstr_long(&cmdstr, ttl);
    if (option) redis_cmd_append_sstr_zstr(&cmdstr, option);

    REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "FIELDS");
    redis_cmd_append_sstr_long(&cmdstr, zend_hash_num_elements(fields));

    ZEND_HASH_FOREACH_VAL(fields, zv)
        field = zval_get_tmp_string(zv, &tmp);
        redis_cmd_append_sstr_zstr(&cmdstr, field);
        zend_tmp_string_release(tmp);
    ZEND_HASH_FOREACH_END();

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

/* MIGRATE */
int redis_migrate_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                      char **cmd, int *cmd_len, short *slot, void **ctx)
{
    zend_string *host = NULL, *key = NULL, *user = NULL, *pass = NULL;
    zend_long destdb = 0, port = 0, timeout = 0;
    zval *zkeys = NULL, *zkey, *zauth = NULL;
    zend_bool copy = 0, replace = 0;
    smart_string cmdstr = {0};
    int argc;

    ZEND_PARSE_PARAMETERS_START(5, 8)
        Z_PARAM_STR(host)
        Z_PARAM_LONG(port)
        Z_PARAM_ZVAL(zkeys)
        Z_PARAM_LONG(destdb)
        Z_PARAM_LONG(timeout)
        Z_PARAM_OPTIONAL
        Z_PARAM_BOOL(copy)
        Z_PARAM_BOOL(replace)
        Z_PARAM_ZVAL_OR_NULL(zauth)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    /* Sanity check on our optional AUTH argument */
    if (zauth && redis_extract_auth_info(zauth, &user, &pass) == FAILURE) {
        php_error_docref(NULL, E_WARNING, "AUTH must be a string or an array with one or two strings");
        user = pass = NULL;
    }

    /* Protect against being passed an array with zero elements */
    if (Z_TYPE_P(zkeys) == IS_ARRAY && zend_hash_num_elements(Z_ARRVAL_P(zkeys)) == 0) {
        php_error_docref(NULL, E_WARNING, "Keys array cannot be empty");
        return FAILURE;
    }

    /* host, port, key|"", dest-db, timeout, [copy, replace] [KEYS key1..keyN] */
    argc = 5 + copy + replace + (user||pass ? 1 : 0) + (user != NULL) + (pass != NULL);
    if (Z_TYPE_P(zkeys) == IS_ARRAY) {
        /* +1 for the "KEYS" argument itself */
        argc += 1 + zend_hash_num_elements(Z_ARRVAL_P(zkeys));
    }

    /* Initialize MIGRATE command with host and port */
    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, argc, "MIGRATE");
    redis_cmd_append_sstr_zstr(&cmdstr, host);
    redis_cmd_append_sstr_long(&cmdstr, port);

    /* If passed a keys array the keys come later, otherwise pass the key to
     * migrate here */
    if (Z_TYPE_P(zkeys) == IS_ARRAY) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "");
    } else {
        key = redis_key_prefix_zval(redis_sock, zkeys);
        redis_cmd_append_sstr_zstr(&cmdstr, key);
        zend_string_release(key);
    }

    redis_cmd_append_sstr_long(&cmdstr, destdb);
    redis_cmd_append_sstr_long(&cmdstr, timeout);
    REDIS_CMD_APPEND_SSTR_OPT_STATIC(&cmdstr, copy, "COPY");
    REDIS_CMD_APPEND_SSTR_OPT_STATIC(&cmdstr, replace, "REPLACE");

    if (user && pass) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "AUTH2");
        redis_cmd_append_sstr_zstr(&cmdstr, user);
        redis_cmd_append_sstr_zstr(&cmdstr, pass);
    } else if (pass) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "AUTH");
        redis_cmd_append_sstr_zstr(&cmdstr, pass);
    }

    /* Append actual keys if we've got a keys array */
    if (Z_TYPE_P(zkeys) == IS_ARRAY) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "KEYS");

        ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(zkeys), zkey) {
            key = redis_key_prefix_zval(redis_sock, zkey);
            redis_cmd_append_sstr_zstr(&cmdstr, key);
            zend_string_release(key);
        } ZEND_HASH_FOREACH_END();
    }

    if (user) zend_string_release(user);
    if (pass) zend_string_release(pass);

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;
    return SUCCESS;
}

/* A generic passthru function for variadic key commands that take one or more
 * keys.  This is essentially all of them except ones that STORE data. */
int redis_varkey_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                     char *kw, char **cmd, int *cmd_len, short *slot, void **ctx)
{
    return gen_varkey_cmd(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock,
                          kw, strlen(kw), 0, cmd, cmd_len, slot);
}

static int
redis_build_client_list_command(smart_string *cmdstr, int argc, zval *z_args)
{
    zend_string *zkey;
    zval *z_ele, *type = NULL, *id = NULL;

    if (argc > 0) {
        if (Z_TYPE(z_args[0]) != IS_ARRAY) {
            return FAILURE;
        }
        ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL(z_args[0]), zkey, z_ele) {
            if (zkey != NULL) {
                ZVAL_DEREF(z_ele);
                if (zend_string_equals_literal_ci(zkey, "type")) {
                    if (Z_TYPE_P(z_ele) != IS_STRING || (
                        !ZVAL_STRICMP_STATIC(z_ele, "normal") &&
                        !ZVAL_STRICMP_STATIC(z_ele, "master") &&
                        !ZVAL_STRICMP_STATIC(z_ele, "replica") &&
                        !ZVAL_STRICMP_STATIC(z_ele, "pubsub")
                    )) {
                        return FAILURE;
                    }
                    type = z_ele;
                } else if (zend_string_equals_literal_ci(zkey, "id")) {
                    if (Z_TYPE_P(z_ele) != IS_STRING && (
                        Z_TYPE_P(z_ele) != IS_ARRAY ||
                        !zend_hash_num_elements(Z_ARRVAL_P(z_ele))
                    )) {
                        return FAILURE;
                    }
                    id = z_ele;
                }
            }
        } ZEND_HASH_FOREACH_END();
    }
    REDIS_CMD_INIT_SSTR_STATIC(cmdstr, 1 + (type ? 2 : 0) + (
        id ? (Z_TYPE_P(id) == IS_ARRAY ? 1 + zend_hash_num_elements(Z_ARRVAL_P(id)) : 2) : 0
    ), "CLIENT");
    REDIS_CMD_APPEND_SSTR_STATIC(cmdstr, "LIST");
    if (type != NULL) {
        REDIS_CMD_APPEND_SSTR_STATIC(cmdstr, "TYPE");
        redis_cmd_append_sstr(cmdstr, Z_STRVAL_P(type), Z_STRLEN_P(type));
    }
    if (id != NULL) {
        REDIS_CMD_APPEND_SSTR_STATIC(cmdstr, "ID");
        if (Z_TYPE_P(id) == IS_ARRAY) {
            ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(id), z_ele) {
                if (Z_TYPE_P(z_ele) == IS_STRING) {
                    redis_cmd_append_sstr(cmdstr, Z_STRVAL_P(z_ele), Z_STRLEN_P(z_ele));
                } else {
                    zkey = zval_get_string(z_ele);
                    redis_cmd_append_sstr(cmdstr, ZSTR_VAL(zkey), ZSTR_LEN(zkey));
                    zend_string_release(zkey);
                }
            } ZEND_HASH_FOREACH_END();
        } else {
            redis_cmd_append_sstr(cmdstr, Z_STRVAL_P(id), Z_STRLEN_P(id));
        }
    }
    return SUCCESS;
}

static int
redis_build_client_kill_command(smart_string *cmdstr, int argc, zval *z_args)
{
    zend_string *zkey;
    zval *z_ele, *id = NULL, *type = NULL, *address = NULL, *opts = NULL,
        *user = NULL, *addr = NULL, *laddr = NULL, *skipme = NULL;

    if (argc > 0) {
        if (argc > 1) {
            if (Z_TYPE(z_args[0]) != IS_STRING || Z_TYPE(z_args[1]) != IS_ARRAY) {
                return FAILURE;
            }
            address = &z_args[0];
            opts = &z_args[1];
        } else if (Z_TYPE(z_args[0]) == IS_STRING) {
            address = &z_args[0];
        } else if (Z_TYPE(z_args[0]) == IS_ARRAY) {
            opts = &z_args[0];
        } else {
            return FAILURE;
        }
        if (opts != NULL) {
            ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(opts), zkey, z_ele) {
                if (zkey != NULL) {
                    ZVAL_DEREF(z_ele);
                    if (Z_TYPE_P(z_ele) != IS_STRING) {
                        return FAILURE;
                    }
                    if (zend_string_equals_literal_ci(zkey, "id")) {
                        id = z_ele;
                    } else if (zend_string_equals_literal_ci(zkey, "type")) {
                        if (!ZVAL_STRICMP_STATIC(z_ele, "normal") &&
                            !ZVAL_STRICMP_STATIC(z_ele, "master") &&
                            !ZVAL_STRICMP_STATIC(z_ele, "slave") &&
                            !ZVAL_STRICMP_STATIC(z_ele, "replica") &&
                            !ZVAL_STRICMP_STATIC(z_ele, "pubsub")
                        ) {
                            return FAILURE;
                        }
                        type = z_ele;
                    } else if (zend_string_equals_literal_ci(zkey, "user")) {
                        user = z_ele;
                    } else if (zend_string_equals_literal_ci(zkey, "addr")) {
                        addr = z_ele;
                    } else if (zend_string_equals_literal_ci(zkey, "laddr")) {
                        laddr = z_ele;
                    } else if (zend_string_equals_literal_ci(zkey, "skipme")) {
                        if (!ZVAL_STRICMP_STATIC(z_ele, "yes") &&
                            !ZVAL_STRICMP_STATIC(z_ele, "no")
                        ) {
                            return FAILURE;
                        }
                        skipme = z_ele;
                    }
                }
            } ZEND_HASH_FOREACH_END();
        }
    }
    REDIS_CMD_INIT_SSTR_STATIC(cmdstr, 1 + (address != 0) + (id ? 2 : 0)
        + (type ? 2 : 0) + (user ? 2 : 0) + (addr ? 2 : 0) + (laddr ? 2 : 0)
        + (skipme ? 2 : 0), "CLIENT");
    REDIS_CMD_APPEND_SSTR_STATIC(cmdstr, "KILL");
    if (address != NULL) {
        redis_cmd_append_sstr(cmdstr, Z_STRVAL_P(address), Z_STRLEN_P(address));
    }
    if (id != NULL) {
        REDIS_CMD_APPEND_SSTR_STATIC(cmdstr, "ID");
        redis_cmd_append_sstr(cmdstr, Z_STRVAL_P(id), Z_STRLEN_P(id));
    }
    if (type != NULL) {
        REDIS_CMD_APPEND_SSTR_STATIC(cmdstr, "TYPE");
        redis_cmd_append_sstr(cmdstr, Z_STRVAL_P(type), Z_STRLEN_P(type));
    }
    if (user != NULL) {
        REDIS_CMD_APPEND_SSTR_STATIC(cmdstr, "USER");
        redis_cmd_append_sstr(cmdstr, Z_STRVAL_P(user), Z_STRLEN_P(user));
    }
    if (addr != NULL) {
        REDIS_CMD_APPEND_SSTR_STATIC(cmdstr, "ADDR");
        redis_cmd_append_sstr(cmdstr, Z_STRVAL_P(addr), Z_STRLEN_P(addr));
    }
    if (laddr != NULL) {
        REDIS_CMD_APPEND_SSTR_STATIC(cmdstr, "LADDR");
        redis_cmd_append_sstr(cmdstr, Z_STRVAL_P(laddr), Z_STRLEN_P(laddr));
    }
    if (skipme != NULL) {
        REDIS_CMD_APPEND_SSTR_STATIC(cmdstr, "SKIPME");
        redis_cmd_append_sstr(cmdstr, Z_STRVAL_P(skipme), Z_STRLEN_P(skipme));
    }
    return SUCCESS;
}

static int
redis_build_client_tracking_command(smart_string *cmdstr, int argc, zval *z_args)
{
    zend_string *zkey;
    zval *z_ele, *redirect = NULL, *prefix = NULL;
    zend_bool bcast = 0, optin = 0, optout = 0, noloop = 0;

    if (argc < 1) {
        return FAILURE;
    }
    if (argc > 1) {
        if (Z_TYPE(z_args[1]) != IS_ARRAY) {
            return FAILURE;
        }
        ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL(z_args[1]), zkey, z_ele) {
            if (zkey != NULL) {
                ZVAL_DEREF(z_ele);
                if (zend_string_equals_literal_ci(zkey, "redirect")) {
                    if (Z_TYPE_P(z_ele) != IS_STRING) {
                        return FAILURE;
                    }
                    redirect = z_ele;
                } else if (zend_string_equals_literal_ci(zkey, "prefix")) {
                    if (Z_TYPE_P(z_ele) != IS_STRING && Z_TYPE_P(z_ele) != IS_ARRAY) {
                        return FAILURE;
                    }
                    prefix = z_ele;
                } else if (zend_string_equals_literal_ci(zkey, "bcast")) {
                    bcast = zval_is_true(z_ele);
                } else if (zend_string_equals_literal_ci(zkey, "optin")) {
                    optin = zval_is_true(z_ele);
                } else if (zend_string_equals_literal_ci(zkey, "optout")) {
                    optout = zval_is_true(z_ele);
                } else if (zend_string_equals_literal_ci(zkey, "noloop")) {
                    noloop = zval_is_true(z_ele);
                }
            }
        } ZEND_HASH_FOREACH_END();
    }
    REDIS_CMD_INIT_SSTR_STATIC(cmdstr, 2 + (redirect ? 2 : 0)
        + (prefix ? 2 * zend_hash_num_elements(Z_ARRVAL_P(prefix)) : 0)
        + bcast + optin + optout + noloop, "CLIENT");
    REDIS_CMD_APPEND_SSTR_STATIC(cmdstr, "TRACKING");
    if (Z_TYPE(z_args[0]) == IS_STRING && (
        ZVAL_STRICMP_STATIC(&z_args[0], "on") ||
        ZVAL_STRICMP_STATIC(&z_args[0], "off")
    )) {
        redis_cmd_append_sstr(cmdstr, Z_STRVAL(z_args[0]), Z_STRLEN(z_args[0]));
    } else if (zval_is_true(&z_args[0])) {
        REDIS_CMD_APPEND_SSTR_STATIC(cmdstr, "ON");
    } else {
        REDIS_CMD_APPEND_SSTR_STATIC(cmdstr, "OFF");
    }
    if (redirect != NULL) {
        REDIS_CMD_APPEND_SSTR_STATIC(cmdstr, "REDIRECT");
        redis_cmd_append_sstr(cmdstr, Z_STRVAL_P(redirect), Z_STRLEN_P(redirect));
    }
    if (prefix != NULL) {
        if (Z_TYPE_P(prefix) == IS_ARRAY) {
            ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(prefix), z_ele) {
                REDIS_CMD_APPEND_SSTR_STATIC(cmdstr, "PREFIX");
                if (Z_TYPE_P(z_ele) == IS_STRING) {
                    redis_cmd_append_sstr(cmdstr, Z_STRVAL_P(z_ele), Z_STRLEN_P(z_ele));
                } else {
                    zkey = zval_get_string(z_ele);
                    redis_cmd_append_sstr(cmdstr, ZSTR_VAL(zkey), ZSTR_LEN(zkey));
                    zend_string_release(zkey);
                }
            } ZEND_HASH_FOREACH_END();
        } else {
            REDIS_CMD_APPEND_SSTR_STATIC(cmdstr, "PREFIX");
            redis_cmd_append_sstr(cmdstr, Z_STRVAL_P(prefix), Z_STRLEN_P(prefix));
        }
    }
    if (bcast) {
        REDIS_CMD_APPEND_SSTR_STATIC(cmdstr, "BCAST");
    }
    if (optin) {
        REDIS_CMD_APPEND_SSTR_STATIC(cmdstr, "OPTIN");
    }
    if (optout) {
        REDIS_CMD_APPEND_SSTR_STATIC(cmdstr, "OPTOUT");
    }
    if (noloop) {
        REDIS_CMD_APPEND_SSTR_STATIC(cmdstr, "NOLOOP");
    }
    return SUCCESS;
}

int
redis_client_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                 char **cmd, int *cmd_len, short *slot, void **ctx)
{
    smart_string cmdstr = {0};
    zend_string *op = NULL;
    zval *z_args = NULL;
    int argc = 0;

    ZEND_PARSE_PARAMETERS_START(1, -1)
        Z_PARAM_STR(op)
        Z_PARAM_OPTIONAL
        Z_PARAM_VARIADIC('*', z_args, argc)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    if (zend_string_equals_literal_ci(op, "INFO")) {
        REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 1, "CLIENT");
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "INFO");
    } else if (zend_string_equals_literal_ci(op, "LIST")) {
        if (redis_build_client_list_command(&cmdstr, argc, z_args) != 0) {
            return FAILURE;
        }
        *ctx = PHPREDIS_CTX_PTR;
    } else if (zend_string_equals_literal_ci(op, "CACHING")) {
        if (argc < 1) {
            return FAILURE;
        }
        REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 2, "CLIENT");
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "CACHING");
        if (Z_TYPE(z_args[0]) == IS_STRING && (
            ZVAL_STRICMP_STATIC(&z_args[0], "yes") ||
            ZVAL_STRICMP_STATIC(&z_args[0], "no")
        )) {
            redis_cmd_append_sstr(&cmdstr, Z_STRVAL(z_args[0]), Z_STRLEN(z_args[0]));
        } else if (zval_is_true(&z_args[0])) {
            REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "YES");
        } else {
            REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "NO");
        }
        *ctx = PHPREDIS_CTX_PTR + 1;
    } else if (zend_string_equals_literal_ci(op, "GETNAME")) {
        REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 1, "CLIENT");
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "GETNAME");
        *ctx = PHPREDIS_CTX_PTR + 3;
    } else if (zend_string_equals_literal_ci(op, "GETREDIR") || zend_string_equals_literal_ci(op, "ID")) {
        REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 1, "CLIENT");
        redis_cmd_append_sstr(&cmdstr, ZSTR_VAL(op), ZSTR_LEN(op));
        *ctx = PHPREDIS_CTX_PTR + 2;
    } else if (zend_string_equals_literal_ci(op, "KILL")) {
        if (redis_build_client_kill_command(&cmdstr, argc, z_args) != 0) {
            return FAILURE;
        }
        *ctx = PHPREDIS_CTX_PTR + 1;
    } else if (zend_string_equals_literal_ci(op, "NO-EVICT")) {
        if (argc < 1) {
            return FAILURE;
        }
        REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 2, "CLIENT");
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "NO-EVICT");
        if (Z_TYPE(z_args[0]) == IS_STRING && (
            ZVAL_STRICMP_STATIC(&z_args[0], "on") ||
            ZVAL_STRICMP_STATIC(&z_args[0], "off")
        )) {
            redis_cmd_append_sstr(&cmdstr, Z_STRVAL(z_args[0]), Z_STRLEN(z_args[0]));
        } else if (zval_is_true(&z_args[0])) {
            REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "ON");
        } else {
            REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "OFF");
        }
        *ctx = PHPREDIS_CTX_PTR + 1;
    } else if (zend_string_equals_literal_ci(op, "PAUSE")) {
        if (argc < 1 || Z_TYPE(z_args[0]) != IS_LONG || (
            argc > 1 && (
                Z_TYPE(z_args[1]) != IS_STRING || (
                    !ZVAL_STRICMP_STATIC(&z_args[1], "write") &&
                    !ZVAL_STRICMP_STATIC(&z_args[1], "all")
                )
            )
        )) {
            return FAILURE;
        }
        REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, argc > 1 ? 3 : 2, "CLIENT");
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "PAUSE");
        redis_cmd_append_sstr_long(&cmdstr, Z_LVAL(z_args[0]));
        if (argc > 1) {
            redis_cmd_append_sstr(&cmdstr, Z_STRVAL(z_args[1]), Z_STRLEN(z_args[1]));
        }
        *ctx = PHPREDIS_CTX_PTR + 1;
    } else if (zend_string_equals_literal_ci(op, "REPLY")) {
        if (argc > 0 && (
            Z_TYPE(z_args[0]) != IS_STRING || (
                !ZVAL_STRICMP_STATIC(&z_args[0], "on") &&
                !ZVAL_STRICMP_STATIC(&z_args[0], "off") &&
                !ZVAL_STRICMP_STATIC(&z_args[0], "skip")
            )
        )) {
            return FAILURE;
        }
        REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, argc > 0 ? 2 : 1, "CLIENT");
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "REPLY");
        if (argc > 0) {
            redis_cmd_append_sstr(&cmdstr, Z_STRVAL(z_args[0]), Z_STRLEN(z_args[0]));
        }
        *ctx = PHPREDIS_CTX_PTR + 1;
    } else if (zend_string_equals_literal_ci(op, "SETNAME")) {
        if (argc < 1 || Z_TYPE(z_args[0]) != IS_STRING) {
            return FAILURE;
        }
        REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 2, "CLIENT");
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "SETNAME");
        redis_cmd_append_sstr(&cmdstr, Z_STRVAL(z_args[0]), Z_STRLEN(z_args[0]));
        *ctx = PHPREDIS_CTX_PTR + 1;
    } else if (zend_string_equals_literal_ci(op, "TRACKING")) {
        if (redis_build_client_tracking_command(&cmdstr, argc, z_args) != 0) {
            return FAILURE;
        }
        *ctx = PHPREDIS_CTX_PTR + 1;
    } else if (zend_string_equals_literal_ci(op, "TRACKINGINFO")) {
        REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 1, "CLIENT");
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "TRACKINGINFO");
        *ctx = PHPREDIS_CTX_PTR + 4;
    } else if (zend_string_equals_literal_ci(op, "UNBLOCK")) {
        if (argc < 1 || Z_TYPE(z_args[0]) != IS_STRING || (
            argc > 1 && (
                Z_TYPE(z_args[1]) != IS_STRING || (
                    !ZVAL_STRICMP_STATIC(&z_args[1], "timeout") &&
                    !ZVAL_STRICMP_STATIC(&z_args[1], "error")
                )
            )
        )) {
            return FAILURE;
        }
        REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, argc > 1 ? 3 : 2, "CLIENT");
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "UNBLOCK");
        redis_cmd_append_sstr(&cmdstr, Z_STRVAL(z_args[0]), Z_STRLEN(z_args[0]));
        if (argc > 1) {
            redis_cmd_append_sstr(&cmdstr, Z_STRVAL(z_args[1]), Z_STRLEN(z_args[1]));
        }
        *ctx = PHPREDIS_CTX_PTR + 2;
    } else if (zend_string_equals_literal_ci(op, "UNPAUSE")) {
        REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 2, "CLIENT");
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "UNPAUSE");
        *ctx = PHPREDIS_CTX_PTR + 1;
    } else {
        return FAILURE;
    }

    // Push out values
    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

/* COMMAND */
int redis_command_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                      char **cmd, int *cmd_len, short *slot, void **ctx)
{
    smart_string cmdstr = {0};
    zend_string *op = NULL, *zstr;
    zval *z_args = NULL;
    int i, argc = 0;

    ZEND_PARSE_PARAMETERS_START(0, -1)
        Z_PARAM_OPTIONAL
        Z_PARAM_STR(op)
        Z_PARAM_VARIADIC('*', z_args, argc)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    if (op == NULL) {
        *ctx = NULL;
        argc = 0;
    } else if (zend_string_equals_literal_ci(op, "COUNT")) {
        *ctx = PHPREDIS_CTX_PTR;
        argc = 0;
    } else if (zend_string_equals_literal_ci(op, "DOCS") ||
        zend_string_equals_literal_ci(op, "INFO")
    ) {
        *ctx = NULL;
    } else if (zend_string_equals_literal_ci(op, "GETKEYS") ||
        zend_string_equals_literal_ci(op, "LIST")
    ) {
        *ctx = PHPREDIS_CTX_PTR + 1;
    } else if (zend_string_equals_literal_ci(op, "GETKEYSANDFLAGS")) {
        *ctx = PHPREDIS_CTX_PTR + 2;
    } else {
        php_error_docref(NULL, E_WARNING, "Unknown COMMAND operation '%s'", ZSTR_VAL(op));
        return FAILURE;
    }

    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, !!op + argc, "COMMAND");
    if (op) redis_cmd_append_sstr_zstr(&cmdstr, op);

    for (i = 0; i < argc; ++i) {
        zstr = zval_get_string(&z_args[i]);
        redis_cmd_append_sstr(&cmdstr, ZSTR_VAL(zstr), ZSTR_LEN(zstr));
        zend_string_release(zstr);
    }

    // Push out values
    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    /* Any slot will do */
    CMD_RAND_SLOT(slot);

    return SUCCESS;
}

int
redis_copy_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
               char **cmd, int *cmd_len, short *slot, void **ctx)
{
    zend_string *src = NULL, *dst = NULL;
    smart_string cmdstr = {0};
    HashTable *opts = NULL;
    zend_bool replace = 0;
    zend_string *zkey;
    zend_long db = -1;
    short slot2;
    zval *zv;

    ZEND_PARSE_PARAMETERS_START(2, 3)
        Z_PARAM_STR(src)
        Z_PARAM_STR(dst)
        Z_PARAM_OPTIONAL
        Z_PARAM_ARRAY_HT_OR_NULL(opts)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    if (opts != NULL) {
        ZEND_HASH_FOREACH_STR_KEY_VAL(opts, zkey, zv) {
            if (zkey == NULL)
                continue;

            ZVAL_DEREF(zv);
            if (zend_string_equals_literal_ci(zkey, "db")) {
                db = zval_get_long(zv);
            } else if (zend_string_equals_literal_ci(zkey, "replace")) {
                replace = zval_is_true(zv);
            }
        } ZEND_HASH_FOREACH_END();
    }

    if (slot && db != -1) {
        php_error_docref(NULL, E_WARNING, "Can't copy to a specific DB in cluster mode");
        return FAILURE;
    }

    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 2 + (db > -1 ? 2 : 0) + replace, "COPY");
    redis_cmd_append_sstr_key_zstr(&cmdstr, src, redis_sock, slot);
    redis_cmd_append_sstr_key_zstr(&cmdstr, dst, redis_sock, slot ? &slot2 : NULL);

    if (slot && *slot != slot2) {
        php_error_docref(NULL, E_WARNING, "Keys must hash to the same slot!");
        efree(cmdstr.c);
        return FAILURE;
    }

    if (db > -1) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "DB");
        redis_cmd_append_sstr_long(&cmdstr, db);
    }
    REDIS_CMD_APPEND_SSTR_OPT_STATIC(&cmdstr, replace, "REPLACE");

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;
    return SUCCESS;
}

/* XADD */
int redis_xadd_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                   char **cmd, int *cmd_len, short *slot, void **ctx)
{
    smart_string cmdstr = {0};
    zend_string *arrkey;
    zval *z_fields, *value;
    zend_long maxlen = 0;
    zend_bool approx = 0, nomkstream = 0;
    zend_ulong idx;
    HashTable *ht_fields;
    int fcount, argc;
    char *key, *id;
    size_t keylen, idlen;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "ssa|lbb", &key, &keylen,
                              &id, &idlen, &z_fields, &maxlen, &approx,
                              &nomkstream) == FAILURE)
    {
        return FAILURE;
    }

    /* At least one field and string are required */
    ht_fields = Z_ARRVAL_P(z_fields);
    if ((fcount = zend_hash_num_elements(ht_fields)) == 0) {
        return FAILURE;
    }

    if (maxlen < 0 || (maxlen == 0 && approx != 0)) {
        php_error_docref(NULL, E_WARNING,
            "Warning:  Invalid MAXLEN argument or approximate flag");
    }


    /* Calculate argc for XADD.  It's a bit complex because we've got
     * an optional MAXLEN argument which can either take the form MAXLEN N
     * or MAXLEN ~ N */
    argc = 2 + nomkstream + (fcount * 2) + (maxlen > 0 ? (approx ? 3 : 2) : 0);

    /* XADD key ID field string [field string ...] */
    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, argc, "XADD");
    redis_cmd_append_sstr_key(&cmdstr, key, keylen, redis_sock, slot);

    if (nomkstream) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "NOMKSTREAM");
    }

    /* Now append our MAXLEN bits if we've got them */
    if (maxlen > 0) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "MAXLEN");
        REDIS_CMD_APPEND_SSTR_OPT_STATIC(&cmdstr, approx, "~");
        redis_cmd_append_sstr_long(&cmdstr, maxlen);
    }

    /* Now append ID and field(s) */
    redis_cmd_append_sstr(&cmdstr, id, idlen);
    ZEND_HASH_FOREACH_KEY_VAL(ht_fields, idx, arrkey, value) {
        redis_cmd_append_sstr_arrkey(&cmdstr, arrkey, idx);
        redis_cmd_append_sstr_zval(&cmdstr, value, redis_sock);
    } ZEND_HASH_FOREACH_END();

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;
    return SUCCESS;
}

// XPENDING key group [start end count [consumer] [idle]]
int redis_xpending_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                       char **cmd, int *cmd_len, short *slot, void **ctx)
{
    zend_string *key = NULL, *group = NULL, *start = NULL, *end = NULL,
                *consumer = NULL;
    zend_long count = -1, idle = 0;
    smart_string cmdstr = {0};
    int argc;

    ZEND_PARSE_PARAMETERS_START(2, 7)
        Z_PARAM_STR(key)
        Z_PARAM_STR(group)
        Z_PARAM_OPTIONAL
        Z_PARAM_STR_OR_NULL(start)
        Z_PARAM_STR_OR_NULL(end)
        Z_PARAM_LONG(count)
        Z_PARAM_STR_OR_NULL(consumer)
        Z_PARAM_LONG(idle)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    /* If we've been passed a start argument, we also need end and count */
    if (start != NULL && (end == NULL || count < 0)) {
        php_error_docref(NULL, E_WARNING, "'$start' must be accompanied by '$end' and '$count' arguments");
        return FAILURE;
    }

    /* Calculate argc.  It's either 2, 5, 6 or 7 */
    argc = 2 + (start != NULL ? 3 + (consumer != NULL) + (idle != 0) : 0);

    /* Construct command and add required arguments */
    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, argc, "XPENDING");
    redis_cmd_append_sstr_key_zstr(&cmdstr, key, redis_sock, slot);
    redis_cmd_append_sstr_zstr(&cmdstr, group);

    /* Add optional argumentst */
    if (start) {
        if (idle != 0) {
            REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "IDLE");
            redis_cmd_append_sstr_long(&cmdstr, (long)idle);
        }
        redis_cmd_append_sstr_zstr(&cmdstr, start);
        redis_cmd_append_sstr_zstr(&cmdstr, end);
        redis_cmd_append_sstr_long(&cmdstr, (long)count);

        /* Finally add consumer if we have it */
        if (consumer) redis_cmd_append_sstr_zstr(&cmdstr, consumer);
    }

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;
    return SUCCESS;
}

/* X[REV]RANGE key start end [COUNT count] */
static int
redis_xrange_generic_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                         char *kw, zend_bool have_count_literal, char **cmd,
                         int *cmd_len, short *slot, void **ctx)
{
    smart_string cmdstr = {0};
    char *key, *start, *end;
    size_t keylen, startlen, endlen;
    zend_long count = -1;
    int argc;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "sss|l", &key, &keylen,
                              &start, &startlen, &end, &endlen, &count)
                              == FAILURE)
    {
        return FAILURE;
    }

    argc = 3 + ((have_count_literal ? 2 : 1) * (count > -1));
    redis_cmd_init_sstr(&cmdstr, argc, kw, strlen(kw));

    redis_cmd_append_sstr_key(&cmdstr, key, keylen, redis_sock, slot);
    redis_cmd_append_sstr(&cmdstr, start, startlen);
    redis_cmd_append_sstr(&cmdstr, end, endlen);

    if (count > -1) {
        if (have_count_literal)
            redis_cmd_append_sstr(&cmdstr, ZEND_STRL("COUNT"));
        redis_cmd_append_sstr_long(&cmdstr, count);
    }

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;
    return SUCCESS;
}

int redis_xrange_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                     char *kw, char **cmd, int *cmd_len, short *slot,
                     void **ctx)
{
    return redis_xrange_generic_cmd(INTERNAL_FUNCTION_PARAM_PASSTHRU,
                                    redis_sock, kw, 1, cmd, cmd_len, slot, ctx);
}

int redis_vrange_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                     char *kw, char **cmd, int *cmd_len, short *slot,
                     void **ctx)
{
    return redis_xrange_generic_cmd(INTERNAL_FUNCTION_PARAM_PASSTHRU,
                                    redis_sock, kw, 0, cmd, cmd_len, slot, ctx);
}

/* Helper function to take an associative array and append the Redis
 * STREAMS stream [stream...] id [id ...] arguments to a command string. */
static int
append_stream_args(smart_string *cmdstr, HashTable *ht, RedisSock *redis_sock,
                   short *slot)
{
    char *kptr, kbuf[40];
    int klen, i, pos = 0;
    zend_string *key, *idstr;
    short oldslot = -1;
    zval **id;
    zend_ulong idx;

    /* Append STREAM qualifier */
    REDIS_CMD_APPEND_SSTR_STATIC(cmdstr, "STREAMS");

    /* Allocate memory to keep IDs */
    id = emalloc(sizeof(*id) * zend_hash_num_elements(ht));

    /* Iterate over our stream => id array appending streams and retaining each
     * value for final arguments */
    ZEND_HASH_FOREACH_KEY_VAL(ht, idx, key, id[pos++]) {
        if (key) {
            klen = ZSTR_LEN(key);
            kptr = ZSTR_VAL(key);
        } else {
            klen = snprintf(kbuf, sizeof(kbuf), "%ld", (long)idx);
            kptr = (char*)kbuf;
        }

        /* Append stream key */
        redis_cmd_append_sstr_key(cmdstr, kptr, klen, redis_sock, slot);

        /* Protect the user against CROSSSLOT to avoid confusion */
        if (slot) {
            if (oldslot != -1 && *slot != oldslot) {
                php_error_docref(NULL, E_WARNING,
                    "Warning, not all keys hash to the same slot!");
                efree(id);
                return FAILURE;
            }
            oldslot = *slot;
        }
    } ZEND_HASH_FOREACH_END();

    /* Add our IDs */
    for (i = 0; i < pos; i++) {
        idstr = zval_get_string(id[i]);
        redis_cmd_append_sstr(cmdstr, ZSTR_VAL(idstr), ZSTR_LEN(idstr));
        zend_string_release(idstr);
    }

    /* Clean up ID container array */
    efree(id);

    return 0;
}

/* XREAD [COUNT count] [BLOCK ms] STREAMS key [key ...] id [id ...] */
int redis_xread_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                    char **cmd, int *cmd_len, short *slot, void **ctx)
{
    smart_string cmdstr = {0};
    zend_long count = -1, block = -1;
    zval *z_streams;
    int argc, scount;
    HashTable *kt;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "a|ll", &z_streams,
                              &count, &block) == FAILURE)
    {
        return FAILURE;
    }

    /* At least one stream and ID is required */
    kt = Z_ARRVAL_P(z_streams);
    if ((scount = zend_hash_num_elements(kt)) < 1) {
        return FAILURE;
    }

    /* Calculate argc and start constructing command */
    argc = 1 + (2 * scount) + (2 * (count > -1)) + (2 * (block > -1));
    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, argc, "XREAD");

    /* Append COUNT if we have it */
    if (count > -1) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "COUNT");
        redis_cmd_append_sstr_long(&cmdstr, count);
    }

    /* Append BLOCK if we have it */
    if (block > -1) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "BLOCK");
        redis_cmd_append_sstr_long(&cmdstr, block);
    }

    /* Append final STREAM key [key ...] id [id ...] arguments */
    if (append_stream_args(&cmdstr, kt, redis_sock, slot) < 0) {
        efree(cmdstr.c);
        return FAILURE;
    }

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;
    return SUCCESS;
}

/* XREADGROUP GROUP group consumer [COUNT count] [BLOCK ms]
 * STREAMS key [key ...] id [id ...] */
int redis_xreadgroup_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                         char **cmd, int *cmd_len, short *slot, void **ctx)
{
    smart_string cmdstr = {0};
    zval *z_streams;
    HashTable *kt;
    char *group, *consumer;
    size_t grouplen, consumerlen;
    int scount, argc;
    zend_long count, block;
    zend_bool no_count = 1, no_block = 1;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "ssa|l!l!", &group,
                              &grouplen, &consumer, &consumerlen, &z_streams,
                              &count, &no_count, &block, &no_block) == FAILURE)
    {
        return FAILURE;
    }

    /* Negative COUNT or BLOCK is illegal so abort immediately */
    if ((!no_count && count < 0) || (!no_block && block < 0)) {
        php_error_docref(NULL, E_WARNING, "Negative values for COUNT or BLOCK are illegal.");
        return FAILURE;
    }

    /* Redis requires at least one stream */
    kt = Z_ARRVAL_P(z_streams);
    if ((scount = zend_hash_num_elements(kt)) < 1) {
        return FAILURE;
    }

    /* Calculate argc and start constructing commands */
    argc = 4 + (2 * scount) + (2 * !no_count) + (2 * !no_block);
    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, argc, "XREADGROUP");

    /* Group and consumer */
    REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "GROUP");
    redis_cmd_append_sstr(&cmdstr, group, grouplen);
    redis_cmd_append_sstr(&cmdstr, consumer, consumerlen);

    /* Append COUNT if we have it */
    if (!no_count) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "COUNT");
        redis_cmd_append_sstr_long(&cmdstr, count);
    }

    /* Append BLOCK argument if we have it */
    if (!no_block) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "BLOCK");
        redis_cmd_append_sstr_long(&cmdstr, block);
    }

    /* Finally append stream and id args */
    if (append_stream_args(&cmdstr, kt, redis_sock, slot) < 0) {
        efree(cmdstr.c);
        return FAILURE;
    }

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;
    return SUCCESS;
}

/* XACK key group id [id ...] */
int redis_xack_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                   char **cmd, int *cmd_len, short *slot, void **ctx)
{
    smart_string cmdstr = {0};
    char *key, *group;
    size_t keylen, grouplen;
    zend_string *idstr;
    zval *z_ids, *z_id;
    HashTable *ht_ids;
    int idcount;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "ssa", &key, &keylen,
                              &group, &grouplen, &z_ids) == FAILURE)
    {
        return FAILURE;
    }

    ht_ids = Z_ARRVAL_P(z_ids);
    if ((idcount = zend_hash_num_elements(ht_ids)) < 1) {
        return FAILURE;
    }

    /* Create command and add initial arguments */
    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 2 + idcount, "XACK");
    redis_cmd_append_sstr_key(&cmdstr, key, keylen, redis_sock, slot);
    redis_cmd_append_sstr(&cmdstr, group, grouplen);

    /* Append IDs */
    ZEND_HASH_FOREACH_VAL(ht_ids, z_id) {
        idstr = zval_get_string(z_id);
        redis_cmd_append_sstr(&cmdstr, ZSTR_VAL(idstr), ZSTR_LEN(idstr));
        zend_string_release(idstr);
    } ZEND_HASH_FOREACH_END();

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;
    return SUCCESS;
}

/* XCLAIM options container */
typedef struct xclaimOptions {
    struct {
        char *type;
        int64_t time;
    } idle;
    zend_long retrycount;
    int force;
    int justid;
} xclaimOptions;

/* Attempt to extract an int64_t from the provided zval */
static int zval_get_i64(zval *zv, int64_t *retval) {
    if (Z_TYPE_P(zv) == IS_LONG) {
        *retval = (int64_t)Z_LVAL_P(zv);
        return SUCCESS;
    } else if (Z_TYPE_P(zv) == IS_DOUBLE) {
        *retval = (int64_t)Z_DVAL_P(zv);
        return SUCCESS;
    } else if (Z_TYPE_P(zv) == IS_STRING) {
        zend_long lval;
        double dval;

        switch (is_numeric_string(Z_STRVAL_P(zv), Z_STRLEN_P(zv), &lval, &dval, 1)) {
            case IS_LONG:
                *retval = (int64_t)lval;
                return SUCCESS;
            case IS_DOUBLE:
                *retval = (int64_t)dval;
                return SUCCESS;
        }
    }

    /* If we make it here we have failed */
    return FAILURE;
}

/* Helper function to get an integer XCLAIM argument.  This can overflow a
 * 32-bit PHP long so we have to extract it as an int64_t.  If the value is
 * not a valid number or negative, we'll inform the user of the problem and
 * that the argument is being ignored. */
static int64_t get_xclaim_i64_arg(const char *key, zval *zv) {
    int64_t retval = -1;

    /* Extract an i64, and if we can't let the user know there is an issue. */
    if (zval_get_i64(zv, &retval) == FAILURE || retval < 0) {
        php_error_docref(NULL, E_WARNING,
            "Invalid XCLAIM option '%s' will be ignored", key);
    }

    return retval;
}

/* Helper to extract XCLAIM options */
static void get_xclaim_options(zval *z_arr, xclaimOptions *opt) {
    zend_string *zkey;
    HashTable *ht;
    zval *zv;

    /* Initialize options array to sane defaults */
    memset(opt, 0, sizeof(*opt));
    opt->retrycount = -1;
    opt->idle.time = -1;

    /* Early return if we don't have any options */
    if (z_arr == NULL)
        return;

    /* Iterate over our options array */
    ht = Z_ARRVAL_P(z_arr);
    ZEND_HASH_FOREACH_STR_KEY_VAL(ht, zkey, zv) {
        if (zkey) {
            if (zend_string_equals_literal_ci(zkey, "TIME")) {
                opt->idle.type = "TIME";
                opt->idle.time = get_xclaim_i64_arg("TIME", zv);
            } else if (zend_string_equals_literal_ci(zkey, "IDLE")) {
                opt->idle.type = "IDLE";
                opt->idle.time = get_xclaim_i64_arg("IDLE", zv);
            } else if (zend_string_equals_literal_ci(zkey, "RETRYCOUNT")) {
                opt->retrycount = zval_get_long(zv);
            }
        } else if (Z_TYPE_P(zv) == IS_STRING) {
            if (zend_string_equals_literal_ci(Z_STR_P(zv), "FORCE")) {
                opt->force = 1;
            } else if (zend_string_equals_literal_ci(Z_STR_P(zv), "JUSTID")) {
                opt->justid = 1;
            }
        }
    } ZEND_HASH_FOREACH_END();
}

/* Count argc for any options we may have */
static int xclaim_options_argc(xclaimOptions *opt) {
    int argc = 0;

    if (opt->idle.type != NULL && opt->idle.time != -1)
        argc += 2;
    if (opt->retrycount != -1)
        argc += 2;
    if (opt->force)
        argc++;
    if (opt->justid)
        argc++;

    return argc;
}

/* Append XCLAIM options */
static void append_xclaim_options(smart_string *cmd, xclaimOptions *opt) {
    /* IDLE/TIME long */
    if (opt->idle.type != NULL && opt->idle.time != -1) {
        redis_cmd_append_sstr(cmd, opt->idle.type, strlen(opt->idle.type));
        redis_cmd_append_sstr_i64(cmd, opt->idle.time);
    }

    /* RETRYCOUNT */
    if (opt->retrycount != -1) {
        REDIS_CMD_APPEND_SSTR_STATIC(cmd, "RETRYCOUNT");
        redis_cmd_append_sstr_long(cmd, opt->retrycount);
    }

    /* FORCE and JUSTID */
    if (opt->force)
        REDIS_CMD_APPEND_SSTR_STATIC(cmd, "FORCE");
    if (opt->justid)
        REDIS_CMD_APPEND_SSTR_STATIC(cmd, "JUSTID");
}


int
redis_xautoclaim_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                     char **cmd, int *cmd_len, short *slot, void **ctx)
{
    smart_string cmdstr = {0};
    char *key, *group, *consumer, *start;
    size_t keylen, grouplen, consumerlen, startlen;
    zend_long min_idle, count = -1;
    zend_bool justid = 0;
    int argc;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "sssls|lb", &key, &keylen,
                              &group, &grouplen, &consumer, &consumerlen,
                              &min_idle, &start, &startlen, &count, &justid
                              ) == FAILURE)
    {
        return FAILURE;
    }

    argc = 5 + (count > 0 ? 2 : 0) + justid;

    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, argc, "XAUTOCLAIM");
    redis_cmd_append_sstr_key(&cmdstr, key, keylen, redis_sock, slot);
    redis_cmd_append_sstr(&cmdstr, group, grouplen);
    redis_cmd_append_sstr(&cmdstr, consumer, consumerlen);
    redis_cmd_append_sstr_long(&cmdstr, min_idle);
    redis_cmd_append_sstr(&cmdstr, start, startlen);

    if (count > 0) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "COUNT");
        redis_cmd_append_sstr_long(&cmdstr, count);
    }

    if (justid) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "JUSTID");
    }

    // Set the context to distinguish XCLAIM from XAUTOCLAIM which
    // have slightly different reply structures.
    *ctx = PHPREDIS_CTX_PTR;

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;
	return SUCCESS;
}

/* XCLAIM <key> <group> <consumer> <min-idle-time> <ID-1> <ID-2>
          [IDLE <milliseconds>] [TIME <mstime>] [RETRYCOUNT <count>]
          [FORCE] [JUSTID] */
int redis_xclaim_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
					 char **cmd, int *cmd_len, short *slot, void **ctx)
{
    smart_string cmdstr = {0};
    char *key, *group, *consumer;
    size_t keylen, grouplen, consumerlen;
    zend_long min_idle;
    int argc, id_count;
    zval *z_ids, *z_id, *z_opts = NULL;
    zend_string *zstr;
    HashTable *ht_ids;
    xclaimOptions opts;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "sssla|a", &key, &keylen,
                              &group, &grouplen, &consumer, &consumerlen, &min_idle,
                              &z_ids, &z_opts) == FAILURE)
    {
        return FAILURE;
    }

    /* At least one id is required */
    ht_ids = Z_ARRVAL_P(z_ids);
    if ((id_count = zend_hash_num_elements(ht_ids)) < 1) {
        return FAILURE;
    }

    /* Extract options array if we've got them */
    get_xclaim_options(z_opts, &opts);

    /* Now we have enough information to calculate argc */
    argc = 4 + id_count + xclaim_options_argc(&opts);

    /* Start constructing our command */
    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, argc, "XCLAIM");
    redis_cmd_append_sstr_key(&cmdstr, key, keylen, redis_sock, slot);
    redis_cmd_append_sstr(&cmdstr, group, grouplen);
    redis_cmd_append_sstr(&cmdstr, consumer, consumerlen);
    redis_cmd_append_sstr_long(&cmdstr, min_idle);

    /* Add IDs */
    ZEND_HASH_FOREACH_VAL(ht_ids, z_id) {
        zstr = zval_get_string(z_id);
        redis_cmd_append_sstr(&cmdstr, ZSTR_VAL(zstr), ZSTR_LEN(zstr));
        zend_string_release(zstr);
    } ZEND_HASH_FOREACH_END();

    /* Finally add our options */
    append_xclaim_options(&cmdstr, &opts);

    /* Success */
    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;
	return SUCCESS;
}

/* XGROUP HELP
 * XGROUP CREATE          key group id       [MKSTREAM] [ENTRIESREAD <n>]
 * XGROUP SETID           key group id                  [ENTRIESREAD <n>]
 * XGROUP CREATECONSUMER  key group consumer
 * XGROUP DELCONSUMER     key group consumer
 * XGROUP DESTROY         key group
 */
int redis_xgroup_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                     char **cmd, int *cmd_len, short *slot, void **ctx)
{
    zend_string *op = NULL, *key = NULL, *group = NULL, *id_or_consumer = NULL;
    int nargs, is_create = 0, is_setid = 0;
    zend_long entries_read = -2;
    smart_string cmdstr = {0};
    zend_bool mkstream = 0;

    ZEND_PARSE_PARAMETERS_START(1, 6)
        Z_PARAM_STR(op)
        Z_PARAM_OPTIONAL
        Z_PARAM_STR(key)
        Z_PARAM_STR(group)
        Z_PARAM_STR(id_or_consumer)
        Z_PARAM_BOOL(mkstream)
        Z_PARAM_LONG(entries_read)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    if (zend_string_equals_literal_ci(op, "HELP")) {
        nargs = 0;
    } else if ((is_create = zend_string_equals_literal_ci(op, "CREATE")) ||
               (is_setid  = zend_string_equals_literal_ci(op, "SETID")) ||
                            zend_string_equals_literal_ci(op, "CREATECONSUMER") ||
                            zend_string_equals_literal_ci(op, "DELCONSUMER"))
    {
        nargs = 3;
    } else if (zend_string_equals_literal_ci(op, "DESTROY")) {
        nargs = 2;
    } else {
        php_error_docref(NULL, E_WARNING, "Unknown XGROUP operation '%s'", ZSTR_VAL(op));
        return FAILURE;
    }

    if (ZEND_NUM_ARGS() < nargs) {
        php_error_docref(NULL, E_WARNING, "Operation '%s' requires %d arguments", ZSTR_VAL(op), nargs);
        return FAILURE;
    }

    mkstream &= is_create;
    if (!(is_create || is_setid))
        entries_read = -2;

    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 1 + nargs + !!mkstream + (entries_read != -2 ? 2 : 0), "XGROUP");
    redis_cmd_append_sstr_zstr(&cmdstr, op);

    if (nargs-- > 0) redis_cmd_append_sstr_key_zstr(&cmdstr, key, redis_sock, slot);
    if (nargs-- > 0) redis_cmd_append_sstr_zstr(&cmdstr, group);
    if (nargs-- > 0) redis_cmd_append_sstr_zstr(&cmdstr, id_or_consumer);

    REDIS_CMD_APPEND_SSTR_OPT_STATIC(&cmdstr, !!mkstream, "MKSTREAM");
    if (entries_read != -2) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "ENTRIESREAD");
        redis_cmd_append_sstr_long(&cmdstr, entries_read);
    }

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

/* XINFO CONSUMERS key group
 * XINFO GROUPS key
 * XINFO STREAM key [FULL [COUNT N]]
 * XINFO HELP */
int redis_xinfo_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                     char **cmd, int *cmd_len, short *slot, void **ctx)
{
    zend_string *op = NULL, *key = NULL, *arg = NULL;
    smart_string cmdstr = {0};
    zend_long count = -1;

    ZEND_PARSE_PARAMETERS_START(1, 4)
        Z_PARAM_STR(op)
        Z_PARAM_OPTIONAL
        Z_PARAM_STR_OR_NULL(key)
        Z_PARAM_STR_OR_NULL(arg)
        Z_PARAM_LONG(count)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    if ((arg != NULL && key == NULL) || (count != -1 && (key == NULL || arg == NULL))) {
        php_error_docref(NULL, E_WARNING, "Cannot pass a non-null optional argument after a NULL one.");
        return FAILURE;
    }

    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 1 + (key != NULL) + (arg != NULL) + (count > -1 ? 2 : 0), "XINFO");
    redis_cmd_append_sstr_zstr(&cmdstr, op);

    if (key != NULL)
        redis_cmd_append_sstr_key(&cmdstr, ZSTR_VAL(key), ZSTR_LEN(key), redis_sock, slot);
    if (arg != NULL)
        redis_cmd_append_sstr_zstr(&cmdstr, arg);

    if (count > -1) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "COUNT");
        redis_cmd_append_sstr_long(&cmdstr, count);
    }

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;
    return SUCCESS;
}

// XTRIM key <MAXLEN | MINID> [= | ~] threshold [LIMIT count]
int redis_xtrim_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                    char **cmd, int *cmd_len, short *slot, void **ctx)
{
    zend_string *key = NULL, *threshold = NULL;
    zend_bool approx = 0, minid = 0;
    smart_string cmdstr = {0};
    zend_long limit = -1;
    int argc;

    ZEND_PARSE_PARAMETERS_START(2, 5)
        Z_PARAM_STR(key)
        Z_PARAM_STR(threshold)
        Z_PARAM_OPTIONAL
        Z_PARAM_BOOL(approx)
        Z_PARAM_BOOL(minid)
        Z_PARAM_LONG(limit)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    argc = 4 + (approx && limit > -1 ? 2 : 0);
    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, argc, "XTRIM");

    redis_cmd_append_sstr_key_zstr(&cmdstr, key, redis_sock, slot);

    if (minid) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "MINID");
    } else {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "MAXLEN");
    }

    if (approx) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "~");
    } else {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "=");
    }

    redis_cmd_append_sstr_zstr(&cmdstr, threshold);

    if (limit > -1 && approx) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "LIMIT");
        redis_cmd_append_sstr_long(&cmdstr, limit);
    } else if (limit > -1) {
        php_error_docref(NULL, E_WARNING, "Cannot use LIMIT without an approximate match, ignoring");
    } else if (ZEND_NUM_ARGS() == 5) {
        php_error_docref(NULL, E_WARNING, "Limit must be >= 0");
    }

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;
    return SUCCESS;
}

// [P]EXPIRE[AT] [NX | XX | GT | LT]
int redis_expire_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                     char *kw, char **cmd, int *cmd_len, short *slot,
                     void **ctx)
{
    zend_string *key = NULL, *mode = NULL;
    smart_string cmdstr = {0};
    zend_long timeout = 0;

    ZEND_PARSE_PARAMETERS_START(2, 3)
        Z_PARAM_STR(key)
        Z_PARAM_LONG(timeout)
        Z_PARAM_OPTIONAL
        Z_PARAM_STR_OR_NULL(mode)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    if (mode != NULL && !(zend_string_equals_literal_ci(mode, "NX") ||
                          zend_string_equals_literal_ci(mode, "XX") ||
                          zend_string_equals_literal_ci(mode, "LT") ||
                          zend_string_equals_literal_ci(mode, "GT")))
    {
        php_error_docref(NULL, E_WARNING, "Unknown expiration modifier '%s'", ZSTR_VAL(mode));
        return FAILURE;
    }

    redis_cmd_init_sstr(&cmdstr, 2 + (mode != NULL), kw, strlen(kw));
    redis_cmd_append_sstr_key_zstr(&cmdstr, key, redis_sock, slot);
    redis_cmd_append_sstr_long(&cmdstr, timeout);
    if (mode != NULL) redis_cmd_append_sstr_zstr(&cmdstr, mode);

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

static int
generic_expiremember_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                         char *kw, size_t kw_len, int has_unit, char **cmd,
                         int *cmd_len, short *slot)
{
    zend_string *key, *mem, *unit = NULL;
    smart_string cmdstr = {0};
    zend_long expiry;

    ZEND_PARSE_PARAMETERS_START(3, has_unit ? 4 : 3)
        Z_PARAM_STR(key)
        Z_PARAM_STR(mem)
        Z_PARAM_LONG(expiry)
        if (has_unit) {
            Z_PARAM_OPTIONAL
            Z_PARAM_STR_OR_NULL(unit)
        }
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    redis_cmd_init_sstr(&cmdstr, 3 + (unit != NULL), kw, kw_len);
    redis_cmd_append_sstr_key_zstr(&cmdstr, key, redis_sock, slot);
    redis_cmd_append_sstr_zstr(&cmdstr, mem);
    redis_cmd_append_sstr_long(&cmdstr, expiry);

    if (unit != NULL) {
        redis_cmd_append_sstr_zstr(&cmdstr, unit);
    }

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}


int redis_expiremember_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                           char **cmd, int *cmd_len, short *slot, void **ctx)
{
    return generic_expiremember_cmd(INTERNAL_FUNCTION_PARAM_PASSTHRU,
                                    redis_sock, ZEND_STRL("EXPIREMEMBER"), 1,
                                    cmd, cmd_len, slot);
}

int redis_expirememberat_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                             char **cmd, int *cmd_len, short *slot, void **ctx)
{
    return generic_expiremember_cmd(INTERNAL_FUNCTION_PARAM_PASSTHRU,
                                    redis_sock, ZEND_STRL("EXPIREMEMBERAT"), 0,
                                    cmd, cmd_len, slot);
}

int
redis_sentinel_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                    char *kw, char **cmd, int *cmd_len, short *slot, void **ctx)
{
    if (zend_parse_parameters_none() == FAILURE) {

        return FAILURE;
    }
    *cmd_len = REDIS_CMD_SPPRINTF(cmd, "SENTINEL", "s", kw, strlen(kw));
    return SUCCESS;
}

int
redis_sentinel_str_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                    char *kw, char **cmd, int *cmd_len, short *slot, void **ctx)
{
    zend_string *name;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "S", &name) == FAILURE) {
        return FAILURE;
    }
    *cmd_len = REDIS_CMD_SPPRINTF(cmd, "SENTINEL", "sS", kw, strlen(kw), name);
    return SUCCESS;
}

typedef enum redisVAddQuant {
    REDIS_QUANT_NONE,
    REDIS_QUANT_NOQUANT,
    REDIS_QUANT_Q8,
    REDIS_QUANT_BIN,
} redisVAddQuant;

typedef struct redisVAddOptions {
    enum redisVAddQuant quant;
    zend_long reduce;
    zend_bool values;
    zend_bool cas;
    zend_long ef;
    zend_string *attributes;
    zend_long numlinks;
} redisVAddOptions;

static zend_bool validate_vadd_integer(zend_string *name, zval *zv, zend_long min) {
    if (Z_TYPE_P(zv) != IS_LONG || Z_LVAL_P(zv) < min) {
        php_error_docref(NULL, E_WARNING, "%s must be >= "
            ZEND_LONG_FMT, ZSTR_VAL(name), min);
        return 0;
    }

    return 1;
}

static zend_string *zval_to_vattr(zval *zv) {
    if (Z_TYPE_P(zv) == IS_STRING) {
        return zval_get_string(zv);
#ifdef HAVE_REDIS_JSON
    } else if (Z_TYPE_P(zv) == IS_ARRAY) {
        smart_str aux = {0};
        php_json_encode(&aux, zv, PHP_JSON_OBJECT_AS_ARRAY);
        return aux.s;
    } else {
        php_error_docref(NULL, E_WARNING, "SETATTR must be a string or array");
    }
#else
    } else {
        php_error_docref(NULL, E_WARNING, "SETATTR must be a string");
    }
#endif

    return NULL;
}

static void parse_vadd_options(redisVAddOptions *dst, HashTable *ht) {
    zend_string *key;
    zval *zv;

    *dst = (redisVAddOptions){
        .ef = -1,
        .numlinks = -1,
        .reduce = -1,
    };

    if (ht == NULL)
        return;

    ZEND_HASH_FOREACH_STR_KEY_VAL(ht, key, zv) {
        if (key != NULL) {
            if (zend_string_equals_literal_ci(key, "REDUCE")) {
                if (validate_vadd_integer(key, zv, 1))
                    dst->reduce = Z_LVAL_P(zv);
            } else if (zend_string_equals_literal_ci(key, "M")) {
                if (validate_vadd_integer(key, zv, 1))
                    dst->numlinks = Z_LVAL_P(zv);
            } else if (zend_string_equals_literal_ci(key, "EF")) {
                if (validate_vadd_integer(key, zv, 1))
                    dst->ef = Z_LVAL_P(zv);
            } else if (zend_string_equals_literal_ci(key, "CAS")) {
                dst->cas = zval_is_true(zv);
            } else if (zend_string_equals_literal_ci(key, "VALUES")) {
                dst->values = zval_is_true(zv);
            } else if (zend_string_equals_literal_ci(key, "SETATTR")) {
                dst->attributes = zval_to_vattr(zv);
            }
        } else if (Z_TYPE_P(zv) == IS_STRING) {
            if (zend_string_equals_literal_ci(Z_STR_P(zv), "VALUES")) {
                dst->values = 1;
            } else if (zend_string_equals_literal_ci(Z_STR_P(zv), "FP32")) {
                dst->values = 0;
            } else if (zend_string_equals_literal_ci(Z_STR_P(zv), "NOQUANT")) {
                dst->quant = REDIS_QUANT_NOQUANT;
            } else if (zend_string_equals_literal_ci(Z_STR_P(zv), "Q8")) {
                dst->quant = REDIS_QUANT_Q8;
            } else if (zend_string_equals_literal_ci(Z_STR_P(zv), "BIN")) {
                dst->quant = REDIS_QUANT_BIN;
            }
        }
    } ZEND_HASH_FOREACH_END();
}

static void free_vadd_options(redisVAddOptions *o) {
    if (o->attributes)
        zend_string_release(o->attributes);
}

static void
redis_cmd_append_sstr_fp32(smart_string *dst, HashTable *ht) {
    uint32_t i = 0;
    char *aux;
    zval *zv;

    aux = ecalloc(1, zend_hash_num_elements(ht) * sizeof(float));

    ZEND_HASH_FOREACH_VAL(ht, zv) {
        redis_copy_fp32(&aux[4 * i++], zval_get_double(zv));
    } ZEND_HASH_FOREACH_END();

    redis_cmd_append_sstr(dst, aux, 4 * i);

    efree(aux);
}

static void redis_cmd_append_sstr_fp32_values(smart_string *dst, HashTable *ht) {
    zval *zv;

    ZEND_HASH_FOREACH_VAL(ht, zv) {
        redis_cmd_append_sstr_dbl(dst, zval_get_double(zv));
    } ZEND_HASH_FOREACH_END();
}

int
redis_vadd_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
               char **cmd, int *cmd_len, short *slot, void **ctx)
{
    HashTable *options = NULL, *vector;
    smart_string cmdstr = {0};
    redisVAddOptions opts;
    zend_string *key;
    zval *element;
    int argc;

    ZEND_PARSE_PARAMETERS_START(3, 4)
        Z_PARAM_STR(key)
        Z_PARAM_ARRAY_HT(vector)
        Z_PARAM_ZVAL(element)
        Z_PARAM_OPTIONAL
        Z_PARAM_ARRAY_HT_OR_NULL(options)
    ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    if (zend_hash_num_elements(vector) < 1) {
        php_error_docref(NULL, E_WARNING,
            "At least one vector element is required");
        return FAILURE;
    }

    parse_vadd_options(&opts, options);

    // Two formulations:
    //   VADD <key> FP32 [vector] <element>
    //   VADD <key> VALUES <n> <v1>..<vn> <element>
    // So <key> and <element> are always present and we need to calculate the
    // other arguments dynamically.
    argc = 2 +
        (opts.reduce > -1 ? 2 : 0) +
        (opts.values ? 2 + zend_hash_num_elements(vector) : 2) +
        !!opts.cas +
        (opts.attributes != NULL ? 2 : 0) +
        (opts.numlinks > 0 ? 2 : 0) +
        (opts.ef > 0 ? 2 : 0) +
        !!(opts.quant != REDIS_QUANT_NONE);

    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, argc, "VADD");
    redis_cmd_append_sstr_key_zstr(&cmdstr, key, redis_sock, slot);

    if (opts.reduce > -1) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "REDUCE");
        redis_cmd_append_sstr_long(&cmdstr, opts.reduce);
    }

    if (opts.values) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "VALUES");
        redis_cmd_append_sstr_long(&cmdstr, zend_hash_num_elements(vector));
        redis_cmd_append_sstr_fp32_values(&cmdstr, vector);
    } else {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "FP32");
        redis_cmd_append_sstr_fp32(&cmdstr, vector);
    }

    redis_cmd_append_sstr_zval(&cmdstr, element, redis_sock);

    if (opts.cas)
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "CAS");

    if (opts.quant != REDIS_QUANT_NONE) {
        if (opts.quant == REDIS_QUANT_Q8) {
            REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "Q8");
        } else if (opts.quant == REDIS_QUANT_BIN) {
            REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "BIN");
        } else {
            REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "NOQUANT");
        }
    }

    if (opts.ef > -1) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "EF");
        redis_cmd_append_sstr_long(&cmdstr, opts.ef);
    }

    if (opts.attributes != NULL) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "SETATTR");
        redis_cmd_append_sstr_zstr(&cmdstr, opts.attributes);
    }

    if (opts.numlinks > -1) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "M");
        redis_cmd_append_sstr_long(&cmdstr, opts.numlinks);
    }

    free_vadd_options(&opts);

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

typedef enum redisVSimMemberType {
    REDIS_VSIM_NONE,
    REDIS_VSIM_FP32,
    REDIS_VSIM_VALUES,
    REDIS_VSIM_ELE,
} redisVSimMemberType;

typedef struct redisVSimOptions {
    redisVSimMemberType type;
    zend_long count;
    zend_long ef;
    zend_string *filter;
    zend_long filter_ef;
    zend_bool truth;
    zend_bool nothread;
    zend_bool withscores;
} redisVSimOptions;

static void free_vsim_options(redisVSimOptions *opts) {
    if (opts->filter != NULL)
        zend_string_release(opts->filter);
}

static void parse_vsim_options(redisVSimOptions *dst, HashTable *ht) {
    zend_string *key;
    zval *zv;

    *dst = (redisVSimOptions){
        .filter_ef = -1,
        .ef = -1,
    };

    if (ht == NULL)
        return;

    ZEND_HASH_FOREACH_STR_KEY_VAL(ht, key, zv) {
        if (key != NULL) {
            if (zend_string_equals_literal_ci(key, "EF")) {
                if (validate_vadd_integer(key, zv, 1))
                    dst->ef = Z_LVAL_P(zv);
            } else if (zend_string_equals_literal_ci(key, "FILTER")) {
                dst->filter = zval_get_string(zv);
            } else if (zend_string_equals_literal_ci(key, "FILTER-EF")) {
                if (validate_vadd_integer(key, zv, 1))
                    dst->filter_ef = Z_LVAL_P(zv);
            } else if (zend_string_equals_literal_ci(key, "COUNT")) {
                if (validate_vadd_integer(key, zv, 1))
                    dst->count = Z_LVAL_P(zv);
            } else if (zend_string_equals_literal_ci(key, "WITHSCORES")) {
                dst->withscores = zval_is_true(zv);
            }
        } else if (Z_TYPE_P(zv) == IS_STRING) {
            if (zend_string_equals_literal_ci(Z_STR_P(zv), "FP32")) {
                dst->type = REDIS_VSIM_FP32;
            } else if (zend_string_equals_literal_ci(Z_STR_P(zv), "VALUES")) {
                dst->type = REDIS_VSIM_VALUES;
            } else if (zend_string_equals_literal_ci(Z_STR_P(zv), "ELE")) {
                dst->type = REDIS_VSIM_ELE;
            } else if (zend_string_equals_literal_ci(Z_STR_P(zv), "NOTHREAD")) {
                dst->nothread = 1;
            } else if (zend_string_equals_literal_ci(Z_STR_P(zv), "TRUTH")) {
                dst->truth = 1;
            } else if (zend_string_equals_literal_ci(Z_STR_P(zv), "WITHSCORES")) {
                dst->withscores = 1;
            }
        }
    } ZEND_HASH_FOREACH_END();
}

static redisVSimMemberType detect_vsim_type(zval *zv) {
    zval *ele;

    if (Z_TYPE_P(zv) != IS_ARRAY)
        return REDIS_VSIM_ELE;

    ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(zv), ele) {
        if (Z_TYPE_P(ele) != IS_LONG && Z_TYPE_P(ele) != IS_DOUBLE)
            return REDIS_VSIM_ELE;
    } ZEND_HASH_FOREACH_END();

    return REDIS_VSIM_FP32;
}

int
redis_vsim_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
               char **cmd, int *cmd_len, short *slot, void **ctx)
{
    HashTable *options = NULL;
    smart_string cmdstr = {0};
    redisVSimOptions opts;
    zend_string *key;
    zval *member;
    int argc;

    ZEND_PARSE_PARAMETERS_START(2, 3) {
        Z_PARAM_STR(key)
        Z_PARAM_ZVAL(member)
        Z_PARAM_OPTIONAL
        Z_PARAM_ARRAY_HT_OR_NULL(options)
    } ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    parse_vsim_options(&opts, options);

    if (opts.type == REDIS_VSIM_NONE)
        opts.type = detect_vsim_type(member);

    /* Sanity check on the member type */
    if (opts.type != REDIS_VSIM_ELE && Z_TYPE_P(member) != IS_ARRAY) {
        php_error_docref(NULL, E_WARNING,
            "member must be an array when not querying in ELE mode");
        return FAILURE;
    }

    argc = 1 + (opts.count > 0 ? 2 : 0) + (opts.ef > 0 ? 2 : 0)
             + (opts.filter != NULL ? 2 : 0) + (opts.filter_ef > 0 ? 2 : 0)
             + !!opts.truth + !!opts.nothread + !!opts.withscores;

    if (opts.type == REDIS_VSIM_ELE || opts.type == REDIS_VSIM_FP32) {
        // ELE <element> or FP32 <fp32 blob>
        argc += 2;
    } else if (opts.type == REDIS_VSIM_VALUES) {
        // VALUES <num> <num values>
        argc += 2 + zend_hash_num_elements(Z_ARRVAL_P(member));
    } else {
        zend_error_noreturn(E_ERROR,
            "Internal error. type shouldn't be REDIS_VSIM_NONE!");
    }

    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, argc, "VSIM");
    redis_cmd_append_sstr_key_zstr(&cmdstr, key, redis_sock, slot);

    if (opts.type == REDIS_VSIM_FP32) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "FP32");
        redis_cmd_append_sstr_fp32(&cmdstr, Z_ARRVAL_P(member));
    } else if (opts.type == REDIS_VSIM_VALUES) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "VALUES");
        redis_cmd_append_sstr_long(&cmdstr,
                                   zend_hash_num_elements(Z_ARRVAL_P(member)));
        redis_cmd_append_sstr_fp32_values(&cmdstr, Z_ARRVAL_P(member));
    } else {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "ELE");
        redis_cmd_append_sstr_zval(&cmdstr, member, redis_sock);
    }

    if (opts.withscores) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "WITHSCORES");
        *ctx = PHPREDIS_CTX_PTR;
    }

    if (opts.count > 0) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "COUNT");
        redis_cmd_append_sstr_long(&cmdstr, opts.count);
    }

    if (opts.ef > 0) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "EF");
        redis_cmd_append_sstr_long(&cmdstr, opts.ef);
    }

    if (opts.filter != NULL) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "FILTER");
        redis_cmd_append_sstr_zstr(&cmdstr, opts.filter);
    }

    if (opts.filter_ef > 0) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "FILTER-EF");
        redis_cmd_append_sstr_long(&cmdstr, opts.filter_ef);
    }

    if (opts.truth)
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "TRUTH");
    if (opts.nothread)
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "NOTHREAD");

    free_vsim_options(&opts);

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

int
redis_vemb_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
               char **cmd, int *cmd_len, short *slot, void **ctx)
{
    smart_string cmdstr = {0};
    zend_bool raw = 0;
    zend_string *key;
    zval *member;

    ZEND_PARSE_PARAMETERS_START(2, 3) {
        Z_PARAM_STR(key)
        Z_PARAM_ZVAL(member);
        Z_PARAM_OPTIONAL
        Z_PARAM_BOOL(raw)
    } ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 2 + !!raw, "VEMB");
    redis_cmd_append_sstr_key_zstr(&cmdstr, key, redis_sock, slot);
    redis_cmd_append_sstr_zval(&cmdstr, member, redis_sock);

    if (raw) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "RAW");
    }

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

int
redis_vlinks_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                 char **cmd, int *cmd_len, short *slot, void **ctx)
{
    smart_string cmdstr = {0};
    zend_bool withscores = 0;
    zend_string *key;
    zval *member;

    ZEND_PARSE_PARAMETERS_START(2, 3) {
        Z_PARAM_STR(key)
        Z_PARAM_ZVAL(member)
        Z_PARAM_OPTIONAL
        Z_PARAM_BOOL(withscores)
    } ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 2 + !!withscores, "VLINKS");
    redis_cmd_append_sstr_key_zstr(&cmdstr, key, redis_sock, slot);
    redis_cmd_append_sstr_zval(&cmdstr, member, redis_sock);
    if (withscores) {
        REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "WITHSCORES");
        *ctx = PHPREDIS_CTX_PTR;
    }

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

int redis_vgetattr_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                       char **cmd, int *cmd_len, short *slot, void **ctx)
{
    smart_string cmdstr = {0};
    zend_bool raw = 0;
    zend_string *key;
    zval *member;

    ZEND_PARSE_PARAMETERS_START(2, 3) {
        Z_PARAM_STR(key)
        Z_PARAM_ZVAL(member)
        Z_PARAM_OPTIONAL
        Z_PARAM_BOOL(raw)
    } ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 2, "VGETATTR");
    redis_cmd_append_sstr_key_zstr(&cmdstr, key, redis_sock, slot);
    redis_cmd_append_sstr_zval(&cmdstr, member, redis_sock);

    *ctx = raw ? NULL : PHPREDIS_CTX_PTR;

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

int
redis_vsetattr_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                   char **cmd, int *cmd_len, short *slot, void **ctx)
{
    smart_string cmdstr = {0};
    zend_string *key, *attr;
    zval *member, *zattr;

    ZEND_PARSE_PARAMETERS_START(3, 3) {
        Z_PARAM_STR(key)
        Z_PARAM_ZVAL(member)
        Z_PARAM_ZVAL(zattr)
    } ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);

    attr = zval_to_vattr(zattr);
    if (zattr == NULL)
        return FAILURE;

    REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 3, "VSETATTR");
    redis_cmd_append_sstr_key_zstr(&cmdstr, key, redis_sock, slot);
    redis_cmd_append_sstr_zval(&cmdstr, member, redis_sock);
    redis_cmd_append_sstr_zstr(&cmdstr, attr);

    zend_string_release(attr);

    *cmd = cmdstr.c;
    *cmd_len = cmdstr.len;

    return SUCCESS;
}

/*
 * Redis commands that don't deal with the server at all.  The RedisSock*
 * pointer is the only thing retrieved differently, so we just take that
 * in addition to the standard INTERNAL_FUNCTION_PARAMETERS for arg parsing,
 * return value handling, and thread safety. */

void redis_getoption_handler(INTERNAL_FUNCTION_PARAMETERS,
                             RedisSock *redis_sock, redisCluster *c)
{
    zend_long option;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "l", &option)
                              == FAILURE)
    {
        RETURN_FALSE;
    }

    // Return the requested option
    switch(option) {
        case REDIS_OPT_SERIALIZER:
            RETURN_LONG(redis_sock->serializer);
        case REDIS_OPT_COMPRESSION:
            RETURN_LONG(redis_sock->compression);
        case REDIS_OPT_COMPRESSION_LEVEL:
            RETURN_LONG(redis_sock->compression_level);
        case REDIS_OPT_PACK_IGNORE_NUMBERS:
            RETURN_BOOL(redis_sock->pack_ignore_numbers);
        case REDIS_OPT_PREFIX:
            if (redis_sock->prefix) {
                RETURN_STRINGL(ZSTR_VAL(redis_sock->prefix), ZSTR_LEN(redis_sock->prefix));
            }
            RETURN_NULL();
        case REDIS_OPT_READ_TIMEOUT:
            RETURN_DOUBLE(redis_sock->read_timeout);
        case REDIS_OPT_TCP_KEEPALIVE:
            RETURN_LONG(redis_sock->tcp_keepalive);
        case REDIS_OPT_SCAN:
            RETURN_LONG(redis_sock->scan);
        case REDIS_OPT_REPLY_LITERAL:
            RETURN_LONG(redis_sock->reply_literal);
        case REDIS_OPT_NULL_MBULK_AS_NULL:
            RETURN_LONG(redis_sock->null_mbulk_as_null);
        case REDIS_OPT_FAILOVER:
            RETURN_LONG(c->failover);
        case REDIS_OPT_MAX_RETRIES:
            RETURN_LONG(redis_sock->max_retries);
        case REDIS_OPT_BACKOFF_ALGORITHM:
            RETURN_LONG(redis_sock->backoff.algorithm);
        case REDIS_OPT_BACKOFF_BASE:
            RETURN_LONG(redis_sock->backoff.base / 1000);
        case REDIS_OPT_BACKOFF_CAP:
            RETURN_LONG(redis_sock->backoff.cap / 1000);
        default:
            RETURN_FALSE;
    }
}

void redis_setoption_handler(INTERNAL_FUNCTION_PARAMETERS,
                             RedisSock *redis_sock, redisCluster *c)
{
    zend_long val_long, option;
    zval *val;
    zend_string *val_str;
    struct timeval read_tv;
    int tcp_keepalive = 0;
    php_netstream_data_t *sock;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "lz", &option,
                              &val) == FAILURE)
    {
        RETURN_FALSE;
    }

    switch(option) {
        case REDIS_OPT_SERIALIZER:
            val_long = zval_get_long(val);
            if (val_long == REDIS_SERIALIZER_NONE
                || val_long == REDIS_SERIALIZER_PHP
                || val_long == REDIS_SERIALIZER_JSON
#ifdef HAVE_REDIS_IGBINARY
                || val_long == REDIS_SERIALIZER_IGBINARY
#endif
#ifdef HAVE_REDIS_MSGPACK
                || val_long == REDIS_SERIALIZER_MSGPACK
#endif
            ) {
                redis_sock->serializer = val_long;
                RETURN_TRUE;
            }
            break;
        case REDIS_OPT_REPLY_LITERAL:
            val_long = zval_get_long(val);
            redis_sock->reply_literal = val_long != 0;
            RETURN_TRUE;
        case REDIS_OPT_NULL_MBULK_AS_NULL:
            val_long = zval_get_long(val);
            redis_sock->null_mbulk_as_null = val_long != 0;
            RETURN_TRUE;
        case REDIS_OPT_COMPRESSION:
            val_long = zval_get_long(val);
            if (val_long == REDIS_COMPRESSION_NONE
#ifdef HAVE_REDIS_LZF
                || val_long == REDIS_COMPRESSION_LZF
#endif
#ifdef HAVE_REDIS_ZSTD
                || val_long == REDIS_COMPRESSION_ZSTD
#endif
#ifdef HAVE_REDIS_LZ4
                || val_long == REDIS_COMPRESSION_LZ4
#endif
            ) {
                redis_sock->compression = val_long;
                RETURN_TRUE;
            }
            break;
        case REDIS_OPT_PACK_IGNORE_NUMBERS:
            redis_sock->pack_ignore_numbers = zval_is_true(val);
            RETURN_TRUE;
        case REDIS_OPT_COMPRESSION_LEVEL:
            val_long = zval_get_long(val);
            redis_sock->compression_level = val_long;
            RETURN_TRUE;
        case REDIS_OPT_PREFIX:
            if (redis_sock->prefix) {
                zend_string_release(redis_sock->prefix);
                redis_sock->prefix = NULL;
            }
            val_str = zval_get_string(val);
            if (ZSTR_LEN(val_str) > 0) {
                redis_sock->prefix = val_str;
            } else {
                zend_string_release(val_str);
            }
            RETURN_TRUE;
        case REDIS_OPT_READ_TIMEOUT:
            redis_sock->read_timeout = zval_get_double(val);
            if (redis_sock->stream) {
                read_tv.tv_sec  = (time_t)redis_sock->read_timeout;
                read_tv.tv_usec = (int)((redis_sock->read_timeout -
                                         read_tv.tv_sec) * 1000000);
                php_stream_set_option(redis_sock->stream,
                                      PHP_STREAM_OPTION_READ_TIMEOUT, 0,
                                      &read_tv);
            }
            RETURN_TRUE;
        case REDIS_OPT_TCP_KEEPALIVE:

            /* Don't set TCP_KEEPALIVE if we're using a unix socket. */
            if (ZSTR_VAL(redis_sock->host)[0] == '/' && redis_sock->port < 1) {
                RETURN_FALSE;
            }
            tcp_keepalive = zval_get_long(val) > 0 ? 1 : 0;
            if (redis_sock->tcp_keepalive == tcp_keepalive) {
                RETURN_TRUE;
            }
            if (redis_sock->stream) {
                /* set TCP_KEEPALIVE */
                sock = (php_netstream_data_t*)redis_sock->stream->abstract;
                if (setsockopt(sock->socket, SOL_SOCKET, SO_KEEPALIVE, (char*)&tcp_keepalive,
                            sizeof(tcp_keepalive)) == -1) {
                    RETURN_FALSE;
                }
                redis_sock->tcp_keepalive = tcp_keepalive;
            }
            RETURN_TRUE;
        case REDIS_OPT_SCAN:
            val_long = zval_get_long(val);
            if (val_long == REDIS_SCAN_NORETRY) {
                redis_sock->scan &= ~REDIS_SCAN_RETRY;
            } else if (val_long == REDIS_SCAN_NOPREFIX) {
                redis_sock->scan &= ~REDIS_SCAN_PREFIX;
            } else if (val_long == REDIS_SCAN_RETRY || val_long == REDIS_SCAN_PREFIX) {
                redis_sock->scan |= val_long;
            } else {
                break;
            }
            RETURN_TRUE;
        case REDIS_OPT_FAILOVER:
            if (c == NULL) RETURN_FALSE;
            val_long = zval_get_long(val);
            if (val_long == REDIS_FAILOVER_NONE ||
                val_long == REDIS_FAILOVER_ERROR ||
                val_long == REDIS_FAILOVER_DISTRIBUTE ||
                val_long == REDIS_FAILOVER_DISTRIBUTE_SLAVES)
            {
                c->failover = val_long;
                RETURN_TRUE;
            }
            break;
        case REDIS_OPT_MAX_RETRIES:
            val_long = zval_get_long(val);
            if(val_long >= 0) {
                redis_sock->max_retries = val_long;
                RETURN_TRUE;
            }
            break;
        case REDIS_OPT_BACKOFF_ALGORITHM:
            val_long = zval_get_long(val);
            if(val_long >= 0 &&
               val_long < REDIS_BACKOFF_ALGORITHMS) {
                redis_sock->backoff.algorithm = val_long;
                RETURN_TRUE;
            }
            break;
        case REDIS_OPT_BACKOFF_BASE:
            val_long = zval_get_long(val);
            if(val_long >= 0) {
                redis_sock->backoff.base = val_long * 1000;
                RETURN_TRUE;
            }
            break;
        case REDIS_OPT_BACKOFF_CAP:
            val_long = zval_get_long(val);
            if(val_long >= 0) {
                redis_sock->backoff.cap = val_long * 1000;
                RETURN_TRUE;
            }
            break;
        default:
            php_error_docref(NULL, E_WARNING, "Unknown option '" ZEND_LONG_FMT "'", option);
            break;
    }
    RETURN_FALSE;
}

void redis_prefix_handler(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock) {
    char *key;
    size_t key_len;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "s", &key, &key_len)
                             ==FAILURE)
    {
        RETURN_FALSE;
    }

    if (redis_sock->prefix) {
        int keyfree = redis_key_prefix(redis_sock, &key, &key_len);
        RETVAL_STRINGL(key, key_len);
        if (keyfree) efree(key);
    } else {
        RETURN_STRINGL(key, key_len);
    }
}

void redis_serialize_handler(INTERNAL_FUNCTION_PARAMETERS,
                             RedisSock *redis_sock)
{
    zval *z_val;
    char *val;
    size_t val_len;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "z", &z_val) == FAILURE) {
        RETURN_FALSE;
    }

    int val_free = redis_serialize(redis_sock, z_val, &val, &val_len);

    RETVAL_STRINGL(val, val_len);
    if (val_free) efree(val);
}

void redis_unserialize_handler(INTERNAL_FUNCTION_PARAMETERS,
                               RedisSock *redis_sock, zend_class_entry *ex)
{
    char *value;
    size_t value_len;

    // Parse our arguments
    if (zend_parse_parameters(ZEND_NUM_ARGS(), "s", &value, &value_len)
                                    == FAILURE)
    {
        RETURN_FALSE;
    }

    // We only need to attempt unserialization if we have a serializer running
    if (redis_sock->serializer == REDIS_SERIALIZER_NONE) {
        // Just return the value that was passed to us
        RETURN_STRINGL(value, value_len);
    }

    zval z_ret;
    if (!redis_unserialize(redis_sock, value, value_len, &z_ret)) {
        // Badly formed input, throw an exception
        zend_throw_exception(ex, "Invalid serialized data, or unserialization error", 0);
        RETURN_FALSE;
    }
    RETURN_ZVAL(&z_ret, 0, 0);
}

void redis_compress_handler(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock) {
    zend_string *zstr;
    size_t len;
    char *buf;
    int cmp_free;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "S", &zstr) == FAILURE) {
        RETURN_FALSE;
    }

    cmp_free = redis_compress(redis_sock, &buf, &len, ZSTR_VAL(zstr), ZSTR_LEN(zstr));
    RETVAL_STRINGL(buf, len);
    if (cmp_free) efree(buf);
}

void redis_uncompress_handler(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
                              zend_class_entry *ex)
{
    zend_string *zstr;
    size_t len;
    char *buf;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "S", &zstr) == FAILURE) {
        RETURN_FALSE;
    } else if (ZSTR_LEN(zstr) == 0 || redis_sock->compression == REDIS_COMPRESSION_NONE) {
        RETURN_STR_COPY(zstr);
    }

    if (!redis_uncompress(redis_sock, &buf, &len, ZSTR_VAL(zstr), ZSTR_LEN(zstr))) {
        zend_throw_exception(ex, "Invalid compressed data or uncompression error", 0);
        RETURN_FALSE;
    }

    RETVAL_STRINGL(buf, len);
    efree(buf);
}

void redis_pack_handler(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock) {
    int valfree;
    size_t len;
    char *val;
    zval *zv;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "z", &zv) == FAILURE) {
        RETURN_FALSE;
    }

    valfree = redis_pack(redis_sock, zv, &val, &len);
    RETVAL_STRINGL(val, len);
    if (valfree) efree(val);
}

void redis_unpack_handler(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock) {
    zend_string *str;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "S", &str) == FAILURE) {
        RETURN_FALSE;
    }

    redis_unpack(redis_sock, ZSTR_VAL(str), ZSTR_LEN(str), return_value);
}
/* vim: set tabstop=4 softtabstop=4 expandtab shiftwidth=4: */
