/*
 * Copyright (c) 2015-2017, Ieshen Zheng <ieshen.zheng at 163 dot com>
 * Copyright (c) 2020, Nick <heronr1 at gmail dot com>
 * Copyright (c) 2020-2021, Bjorn Svensson <bjorn.a.svensson at est dot tech>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   * Redistributions of source code must retain the above copyright notice,
 *     this list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *   * Neither the name of Redis nor the names of its contributors may be used
 *     to endorse or promote products derived from this software without
 *     specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
#include <ctype.h>
#include <errno.h>
#include <hiredis/alloc.h>
#ifndef _WIN32
#include <alloca.h>
#include <strings.h>
#else
#include <malloc.h>
#endif
#include <string.h>

#include "command.h"
#include "hiarray.h"
#include "hiutil.h"
#include "win32.h"

#define LF (uint8_t)10
#define CR (uint8_t)13

static uint64_t cmd_id = 0; /* command id counter */

typedef enum {
    KEYPOS_NONE,
    KEYPOS_UNKNOWN,
    KEYPOS_INDEX,
    KEYPOS_KEYNUM
} cmd_keypos;

typedef struct {
    cmd_type_t type;           /* A constant identifying the command. */
    const char *name;          /* Command name */
    const char *subname;       /* Subcommand name or NULL */
    cmd_keypos firstkeymethod; /* First key none, unknown, pos or keynum */
    int8_t firstkeypos;        /* Position of first key or the  arg */
    int8_t arity;              /* Arity, neg number means min num args */
} cmddef;

/* Populate the table with code in cmddef.h generated from Redis JSON files. */
static cmddef redis_commands[] = {
#define COMMAND(_type, _name, _subname, _arity, _keymethod, _keypos)           \
    {.type = CMD_REQ_REDIS_##_type,                                            \
     .name = _name,                                                            \
     .subname = _subname,                                                      \
     .firstkeymethod = KEYPOS_##_keymethod,                                    \
     .firstkeypos = _keypos,                                                   \
     .arity = _arity},
#include "cmddef.h"
#undef COMMAND
};

static inline void to_upper(char *dst, const char *src, uint32_t len) {
    uint32_t i;
    for (i = 0; i < len; i++) {
        if (src[i] >= 'a' && src[i] <= 'z')
            dst[i] = src[i] - ('a' - 'A');
        else
            dst[i] = src[i];
    }
}

/* Looks up a command or subcommand in the command table. Arg0 and arg1 are used
 * to lookup the command. The function returns CMD_UNKNOWN on failure. On
 * success, the command type is returned and *firstkey and *arity are
 * populated. */
cmddef *redis_lookup_cmd(const char *arg0, uint32_t arg0_len, const char *arg1,
                         uint32_t arg1_len) {
    int num_commands = sizeof(redis_commands) / sizeof(cmddef);
    /* Compare command name in uppercase. */
    char *cmd = alloca(arg0_len);
    to_upper(cmd, arg0, arg0_len);
    char *subcmd = NULL; /* Alloca later on demand. */
    /* Find the command using binary search. */
    int left = 0, right = num_commands - 1;
    while (left <= right) {
        int i = (left + right) / 2;
        cmddef *c = &redis_commands[i];

        int cmp = strncmp(c->name, cmd, arg0_len);
        if (cmp == 0 && strlen(c->name) > arg0_len)
            cmp = 1; /* "HGETALL" vs "HGET" */

        /* If command name matches, compare subcommand if any */
        if (cmp == 0 && c->subname != NULL) {
            if (arg1 == NULL) {
                /* Command has subcommands, but none given. */
                return NULL;
            }
            if (subcmd == NULL) {
                subcmd = alloca(arg1_len);
                to_upper(subcmd, arg1, arg1_len);
            }
            cmp = strncmp(c->subname, subcmd, arg1_len);
            if (cmp == 0 && strlen(c->subname) > arg1_len)
                cmp = 1;
        }

        if (cmp < 0) {
            left = i + 1;
        } else if (cmp > 0) {
            right = i - 1;
        } else {
            /* Found it. */
            return c;
        }
    }
    return NULL;
}

/*
 * Return true, if the redis command is a vector command accepting one or
 * more keys, otherwise return false
 * Format: command key [ key ... ]
 */
static int redis_argx(struct cmd *r) {
    switch (r->type) {
    case CMD_REQ_REDIS_EXISTS:
    case CMD_REQ_REDIS_MGET:
    case CMD_REQ_REDIS_DEL:
        return 1;

    default:
        break;
    }

    return 0;
}

/*
 * Return true, if the redis command is a vector command accepting one or
 * more key-value pairs, otherwise return false
 * Format: command key value [ key value ... ]
 */
static int redis_argkvx(struct cmd *r) {
    switch (r->type) {
    case CMD_REQ_REDIS_MSET:
        return 1;

    default:
        break;
    }

    return 0;
}

/* Parses a bulk string starting at 'p' and ending somewhere before 'end'.
 * Returns the remaining of the input after consuming the bulk string. The
 * pointers *str and *len are pointed to the parsed string and its length. On
 * parse error, NULL is returned. */
char *redis_parse_bulk(char *p, char *end, char **str, uint32_t *len) {
    uint32_t length = 0;
    if (p >= end || *p++ != '$')
        return NULL;
    while (p < end && *p >= '0' && *p <= '9') {
        length = length * 10 + (uint32_t)(*p++ - '0');
    }
    if (p >= end || *p++ != CR)
        return NULL;
    if (p >= end || *p++ != LF)
        return NULL;
    if (str)
        *str = p;
    if (len)
        *len = length;
    p += length;
    if (p >= end || *p++ != CR)
        return NULL;
    if (p >= end || *p++ != LF)
        return NULL;
    return p;
}

static inline int push_keypos(struct cmd *r, char *arg, uint32_t arglen) {
    struct keypos *kpos = hiarray_push(r->keys);
    if (kpos == NULL)
        return 0;
    kpos->start = arg;
    kpos->end = arg + arglen;
    return 1;
}

/*
 * Reference: http://redis.io/topics/protocol
 *
 * Redis >= 1.2 uses the unified protocol to send requests to the Redis
 * server. In the unified protocol all the arguments sent to the server
 * are binary safe and every request has the following general form:
 *
 *   *<number of arguments> CR LF
 *   $<number of bytes of argument 1> CR LF
 *   <argument data> CR LF
 *   ...
 *   $<number of bytes of argument N> CR LF
 *   <argument data> CR LF
 *
 * Before the unified request protocol, redis protocol for requests supported
 * the following commands
 * 1). Inline commands: simple commands where arguments are just space
 *     separated strings. No binary safeness is possible.
 * 2). Bulk commands: bulk commands are exactly like inline commands, but
 *     the last argument is handled in a special way in order to allow for
 *     a binary-safe last argument.
 *
 * only supports the Redis unified protocol for requests.
 */
void redis_parse_cmd(struct cmd *r) {
    ASSERT(r->cmd != NULL && r->clen > 0);
    char *p = r->cmd;
    char *end = r->cmd + r->clen;
    uint32_t rnarg = 0;                  /* Number of args including cmd name */
    int argidx = -1;                     /* Index of last parsed arg */
    char *arg;                           /* Last parsed arg */
    uint32_t arglen;                     /* Length of arg */
    char *arg0 = NULL, *arg1 = NULL;     /* The first two args */
    uint32_t arg0_len = 0, arg1_len = 0; /* Lengths of arg0 and arg1 */
    cmddef *info = NULL;                 /* Command info, when found */

    /* Check that the command line is multi-bulk. */
    if (*p++ != '*')
        goto error;

    /* Parse multi-bulk size (rnarg). */
    while (p < end && *p >= '0' && *p <= '9') {
        rnarg = rnarg * 10 + (uint32_t)(*p++ - '0');
    }
    if (p == end || *p++ != CR)
        goto error;
    if (p == end || *p++ != LF)
        goto error;
    if (rnarg == 0)
        goto error;
    r->narg = rnarg;

    /* Parse the first two args. */
    if ((p = redis_parse_bulk(p, end, &arg0, &arg0_len)) == NULL)
        goto error;
    argidx++;
    if (rnarg > 1) {
        if ((p = redis_parse_bulk(p, end, &arg1, &arg1_len)) == NULL)
            goto error;
        argidx++;
    }

    /* Lookup command. */
    if ((info = redis_lookup_cmd(arg0, arg0_len, arg1, arg1_len)) == NULL)
        goto error; /* Command not found. */
    r->type = info->type;

    /* Arity check (negative arity means minimum num args) */
    if ((info->arity >= 0 && (int)rnarg != info->arity) ||
        (info->arity < 0 && (int)rnarg < -info->arity)) {
        goto error;
    }
    if (info->firstkeymethod == KEYPOS_NONE)
        goto done; /* Command takes no keys. */
    if (arg1 == NULL)
        goto error; /* Command takes keys, but no args given. Quick abort. */

    /* Below we assume arg1 != NULL, */

    /* Handle commands where firstkey depends on special logic. */
    if (info->firstkeymethod == KEYPOS_UNKNOWN) {
        /* Keyword-based first key position */
        const char *keyword;
        int startfrom;
        if (r->type == CMD_REQ_REDIS_XREAD) {
            keyword = "STREAMS";
            startfrom = 1;
        } else if (r->type == CMD_REQ_REDIS_XREADGROUP) {
            keyword = "STREAMS";
            startfrom = 4;
        } else {
            /* Not reached, but can be reached if Redis adds more commands. */
            goto error;
        }

        /* Skip forward to the 'startfrom' arg index, then search for the keyword. */
        arg = arg1;
        arglen = arg1_len;
        while (argidx < (int)rnarg - 1) {
            if ((p = redis_parse_bulk(p, end, &arg, &arglen)) == NULL)
                goto error; /* Keyword not provided, thus no keys. */
            if (argidx++ < startfrom)
                continue; /* Keyword can't appear in a position before 'startfrom' */
            if (!strncasecmp(keyword, arg, arglen)) {
                /* Keyword found. Now the first key is the next arg. */
                if ((p = redis_parse_bulk(p, end, &arg, &arglen)) == NULL)
                    goto error;
                if (!push_keypos(r, arg, arglen))
                    goto oom;
                goto done;
            }
        }

        /* Keyword not provided. */
        goto error;
    }

    /* Find first key arg. */
    arg = arg1;
    arglen = arg1_len;
    for (; argidx < info->firstkeypos; argidx++) {
        if ((p = redis_parse_bulk(p, end, &arg, &arglen)) == NULL)
            goto error;
    }

    if (info->firstkeymethod == KEYPOS_KEYNUM) {
        /* The arg specifies the number of keys and the first key is the next
         * arg. Example:
         *
         * EVAL script numkeys [key [key ...]] [arg [arg ...]] */
        if (!strncmp("0", arg, arglen))
            goto done; /* No args. */
        /* One or more args. The first key is the arg after the 'numkeys' arg. */
        if ((p = redis_parse_bulk(p, end, &arg, &arglen)) == NULL)
            goto error;
        argidx++;
    }

    /* Now arg is the first key and arglen is its length. */

    if (info->type == CMD_REQ_REDIS_MIGRATE && arglen == 0 &&
        info->firstkeymethod == KEYPOS_INDEX && info->firstkeypos == 3) {
        /* MIGRATE host port <key | ""> destination-db timeout [COPY] [REPLACE]
         * [[AUTH password] | [AUTH2 username password]] [KEYS key [key ...]]
         *
         * The key spec points out arg3 as the first key, but if it's an empty
         * string, we would need to search for the KEYS keyword arg backwards
         * from the end of the command line. This is not implemented. */
        goto error;
    }

    if (!push_keypos(r, arg, arglen))
        goto oom;

    /* Special commands where we want all keys (not only the first key). */
    if (redis_argx(r) || redis_argkvx(r)) {
        /* argx:   MGET key [ key ... ] */
        /* argkvx: MSET key value [ key value ... ] */
        if (redis_argkvx(r) && rnarg % 2 == 0)
            goto error;
        for (uint32_t i = 2; i < rnarg; i++) {
            if ((p = redis_parse_bulk(p, end, &arg, &arglen)) == NULL)
                goto error;
            if (redis_argkvx(r) && i % 2 == 0)
                continue; /* not a key */
            if (!push_keypos(r, arg, arglen))
                goto oom;
        }
    }

done:
    ASSERT(r->type > CMD_UNKNOWN && r->type < CMD_SENTINEL);
    r->result = CMD_PARSE_OK;
    return;

error:
    r->result = CMD_PARSE_ERROR;
    errno = EINVAL;
    size_t errmaxlen = 100; /* Enough for the error messages below. */
    if (r->errstr == NULL) {
        r->errstr = hi_malloc(errmaxlen);
        if (r->errstr == NULL) {
            goto oom;
        }
    }

    if (info != NULL && info->subname != NULL)
        snprintf(r->errstr, errmaxlen, "Failed to find keys of command %s %s",
                 info->name, info->subname);
    else if (info != NULL)
        snprintf(r->errstr, errmaxlen, "Failed to find keys of command %s",
                 info->name);
    else if (r->type == CMD_UNKNOWN && arg0 != NULL && arg1 != NULL)
        snprintf(r->errstr, errmaxlen, "Unknown command %.*s %.*s", arg0_len,
                 arg0, arg1_len, arg1);
    else if (r->type == CMD_UNKNOWN && arg0 != NULL)
        snprintf(r->errstr, errmaxlen, "Unknown command %.*s", arg0_len, arg0);
    else
        snprintf(r->errstr, errmaxlen, "Command parse error");
    return;

oom:
    r->result = CMD_PARSE_ENOMEM;
}

struct cmd *command_get(void) {
    struct cmd *command;
    command = hi_malloc(sizeof(struct cmd));
    if (command == NULL) {
        return NULL;
    }

    command->id = ++cmd_id;
    command->result = CMD_PARSE_OK;
    command->errstr = NULL;
    command->type = CMD_UNKNOWN;
    command->cmd = NULL;
    command->clen = 0;
    command->keys = NULL;
    command->narg = 0;
    command->quit = 0;
    command->noforward = 0;
    command->slot_num = -1;
    command->frag_seq = NULL;
    command->reply = NULL;
    command->sub_commands = NULL;
    command->node_addr = NULL;

    command->keys = hiarray_create(1, sizeof(struct keypos));
    if (command->keys == NULL) {
        hi_free(command);
        return NULL;
    }

    return command;
}

void command_destroy(struct cmd *command) {
    if (command == NULL) {
        return;
    }

    if (command->cmd != NULL) {
        hi_free(command->cmd);
        command->cmd = NULL;
    }

    if (command->errstr != NULL) {
        hi_free(command->errstr);
        command->errstr = NULL;
    }

    if (command->keys != NULL) {
        command->keys->nelem = 0;
        hiarray_destroy(command->keys);
        command->keys = NULL;
    }

    if (command->frag_seq != NULL) {
        hi_free(command->frag_seq);
        command->frag_seq = NULL;
    }

    freeReplyObject(command->reply);

    if (command->sub_commands != NULL) {
        listRelease(command->sub_commands);
    }

    if (command->node_addr != NULL) {
        sdsfree(command->node_addr);
        command->node_addr = NULL;
    }

    hi_free(command);
}
