#ifndef DDEBUG
#define DDEBUG 0
#endif
#include "ddebug.h"

#include <lua.h>
#include <lauxlib.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>


#define LUA_REDIS_PARSER_VERSION "0.13"


enum {
    BAD_REPLY           = 0,
    STATUS_REPLY        = 1,
    ERROR_REPLY         = 2,
    INTEGER_REPLY       = 3,
    BULK_REPLY          = 4,
    MULTI_BULK_REPLY    = 5
};


enum {
    PARSE_OK    = 0,
    PARSE_ERROR = 1
};


static const char *redis_typenames[] = {
    "bad reply",
    "status reply",
    "error reply",
    "integer reply",
    "bulk reply",
    "multi-bulk reply"
};


#define UINT64_LEN   (sizeof("18446744073709551615") - 1)

static void *redis_null = NULL;

static int parse_reply_helper(lua_State *L, char **src, size_t len);
static int redis_parse_reply(lua_State *L);
static int redis_parse_replies(lua_State *L);
static int redis_build_query(lua_State *L);
static const char * parse_single_line_reply(const char *src, const char *last,
        size_t *dst_len);
static const char * parse_bulk_reply(const char *src, const char *last,
        size_t *dst_len);
static int parse_multi_bulk_reply(lua_State *L, char **src,
        const char *last);
static size_t get_num_size(size_t i);
static char *sprintf_num(char *dst, int64_t ui64);
static int redis_typename(lua_State *L);


#define redis_nelems(arr) \
            (sizeof(arr) / sizeof(arr[0]))


static const struct luaL_Reg redis_parser[] = {
    {"parse_reply", redis_parse_reply},
    {"parse_replies", redis_parse_replies},
    {"build_query", redis_build_query},
    {"typename", redis_typename},
    {NULL, NULL}
};


int
luaopen_redis_parser(lua_State *L)
{
    luaL_register(L, "redis.parser", redis_parser);

    lua_pushliteral(L, LUA_REDIS_PARSER_VERSION);
    lua_setfield(L, -2, "_VERSION");

    lua_pushlightuserdata(L, redis_null);
    lua_setfield(L, -2, "null");

    lua_pushnumber(L, BAD_REPLY);
    lua_setfield(L, -2, "BAD_REPLY");

    lua_pushnumber(L, STATUS_REPLY);
    lua_setfield(L, -2, "STATUS_REPLY");

    lua_pushnumber(L, ERROR_REPLY);
    lua_setfield(L, -2, "ERROR_REPLY");

    lua_pushnumber(L, INTEGER_REPLY);
    lua_setfield(L, -2, "INTEGER_REPLY");

    lua_pushnumber(L, BULK_REPLY);
    lua_setfield(L, -2, "BULK_REPLY");

    lua_pushnumber(L, MULTI_BULK_REPLY);
    lua_setfield(L, -2, "MULTI_BULK_REPLY");

    return 1;
}


static int
redis_parse_reply(lua_State *L)
{
    char            *p;
    size_t           len;

    if (lua_gettop(L) != 1) {
        return luaL_error(L, "expected one argument but got %d",
                lua_gettop(L));
    }

    p = (char *) luaL_checklstring(L, 1, &len);

    return parse_reply_helper(L, &p, len);
}


static int
redis_parse_replies(lua_State *L)
{
    char        *p;
    char        *q;
    int          i, n, nret;
    size_t       len;

    if (lua_gettop(L) != 2) {
        return luaL_error(L, "expected two arguments but got %d",
                lua_gettop(L));
    }

    p = (char *) luaL_checklstring(L, 1, &len);

    n = luaL_checknumber(L, 2);

    dd("n = %d", (int) n);

    lua_pop(L, 1);

    lua_createtable(L, n, 0); /* table */

    for (i = 1; i <= n; i++) {
        dd("parsing reply %d", i);

        lua_createtable(L, n, 2); /* table table */
        q = p;
        nret = parse_reply_helper(L, &p, len); /* table table res typ */
        if (nret != 2) {
            return luaL_error(L, "internal error: redis_parse_reply "
                    "returns %d", nret);
        }

        dd("p = %p, q = %p, len = %d", p, q, (int) len);

        len -= p - q;

        dd("len is now %d", (int) len);

        lua_rawseti(L, -3, 2); /* table table res */
        lua_rawseti(L, -2, 1); /* table table */
        lua_rawseti(L, -2, i); /* table */
    }

    return 1;
}


static int
parse_reply_helper(lua_State *L, char **src, size_t len)
{
    char            *p;
    const char      *last;
    const char      *dst;
    size_t           dst_len;
    lua_Number       num;
    int              rc;

    p = *src;

    if (len == 0) {
        lua_pushliteral(L, "empty reply");
        lua_pushnumber(L, BAD_REPLY);
        return 2;
    }

    last = p + len;

    switch (*p) {
    case '+':
        p++;
        dst = parse_single_line_reply(p, last, &dst_len);

        if (dst_len == -2) {
            lua_pushliteral(L, "bad status reply");
            lua_pushnumber(L, BAD_REPLY);
            return 2;
        }

        *src += dst_len + 1 + sizeof("\r\n") - 1;

        lua_pushlstring(L, dst, dst_len);
        lua_pushnumber(L, STATUS_REPLY);
        break;

    case '-':
        p++;
        dst = parse_single_line_reply(p, last, &dst_len);

        if (dst_len == -2) {
            lua_pushliteral(L, "bad error reply");
            lua_pushnumber(L, BAD_REPLY);
            return 2;
        }

        *src += dst_len + 1 + sizeof("\r\n") - 1;

        lua_pushlstring(L, dst, dst_len);
        lua_pushnumber(L, ERROR_REPLY);
        break;

    case ':':
        p++;
        dst = parse_single_line_reply(p, last, &dst_len);

        if (dst_len == -2) {
            lua_pushliteral(L, "bad integer reply");
            lua_pushnumber(L, BAD_REPLY);
            return 2;
        }

        *src += dst_len + 1 + sizeof("\r\n") - 1;

        lua_pushlstring(L, dst, dst_len);
        num = lua_tonumber(L, -1);
        lua_pop(L, 1);

        lua_pushnumber(L, num);
        lua_pushnumber(L, INTEGER_REPLY);
        break;

    case '$':
        p++;
        dst = parse_bulk_reply(p, last, &dst_len);

        if (dst_len == -2) {
            lua_pushliteral(L, "bad bulk reply");
            lua_pushnumber(L, BAD_REPLY);
            return 2;
        }

        if (dst_len == -1) {
            *src = (char *) dst + sizeof("\r\n") - 1;

            /* lua_pushlightuserdata(L, redis_null); */
            lua_pushnil(L);
            lua_pushnumber(L, BULK_REPLY);
            return 2;
        }

        *src = (char *) dst + dst_len + sizeof("\r\n") - 1;

        lua_pushlstring(L, dst, dst_len);
        lua_pushnumber(L, BULK_REPLY);
        break;

    case '*':
        p++;
        rc = parse_multi_bulk_reply(L, &p, last);

        if (rc != PARSE_OK) {
            lua_pushliteral(L, "bad multi bulk reply");
            lua_pushnumber(L, BAD_REPLY);
            return 2;
        }

        /* rc == PARSE_OK */

        *src = (char *) p;

        lua_pushnumber(L, MULTI_BULK_REPLY);
        break;

    default:
        lua_pushliteral(L, "invalid reply");
        lua_pushnumber(L, BAD_REPLY);
        break;
    }

    return 2;
}


static const char *
parse_single_line_reply(const char *src, const char *last, size_t *dst_len)
{
    const char  *p = src;
    int          seen_cr = 0;

    while (p != last) {

        if (*p == '\r') {
            seen_cr = 1;

        } else if (seen_cr) {
            if (*p == '\n') {
                *dst_len = p - src - 1;
                return src;
            }

            seen_cr = 0;
        }

        p++;
    }

    /* CRLF not found at all */
    *dst_len = -2;
    return NULL;
}


#define CHECK_EOF if (p >= last) goto invalid;


static const char *
parse_bulk_reply(const char *src, const char *last, size_t *dst_len)
{
    const char *p = src;
    ssize_t     size = 0;
    const char *dst;

    CHECK_EOF

    /* read the bulk size */

    if (*p == '-') {
        p++;
        CHECK_EOF

        while (*p != '\r') {
            if (*p < '0' || *p > '9') {
                goto invalid;
            }

            p++;
            CHECK_EOF
        }

        /* *p == '\r' */

        if (last - p < size + sizeof("\r\n") - 1) {
            goto invalid;
        }

        p++;

        if (*p++ != '\n') {
            goto invalid;
        }

        *dst_len = -1;
        return p - (sizeof("\r\n") - 1);
    }

    while (*p != '\r') {
        if (*p < '0' || *p > '9') {
            goto invalid;
        }

        size *= 10;
        size += *p - '0';

        p++;
        CHECK_EOF
    }

    /* *p == '\r' */

    p++;
    CHECK_EOF

    if (*p++ != '\n') {
        goto invalid;
    }

    /* read the bulk data */

    if (last - p < size + sizeof("\r\n") - 1) {
        goto invalid;
    }

    dst = p;

    p += size;

    if (*p++ != '\r') {
        goto invalid;
    }

    if (*p++ != '\n') {
        goto invalid;
    }

    *dst_len = size;
    return dst;

invalid:
    *dst_len = -2;
    return NULL;
}


static int
parse_multi_bulk_reply(lua_State *L, char **src, const char *last)
{
    const char      *p = *src;
    int              count = 0;
    int              i;
    size_t           dst_len;
    const char      *dst;

    dd("enter multi bulk parser");

    CHECK_EOF

    while (*p != '\r') {
        if (*p == '-') {

            p++;
            CHECK_EOF

            if (*p < '0' || *p > '9') {
                goto invalid;
            }

            while (*p != '\r') {
                p++;
                CHECK_EOF
            }

            count = -1;
            break;
        }

        if (*p < '0' || *p > '9') {
            dd("expecting digit, but found %c", *p);
            goto invalid;
        }

        count *= 10;
        count += *p - '0';

        p++;
        CHECK_EOF
    }

    dd("count = %d", count);

    /* *p == '\r' */

    p++;
    CHECK_EOF

    if (*p++ != '\n') {
        goto invalid;
    }

    dd("reading the individual bulks");

    if (count == -1) {

        /* lua_pushlightuserdata(L, redis_null); */
        lua_pushnil(L);

        *src = (char *) p;
        return PARSE_OK;
    }

    lua_createtable(L, count, 0);

    for (i = 1; i <= count; i++) {
        CHECK_EOF

        switch (*p) {
        case '+':
        case '-':
        case ':':
            p++;
            dst = parse_single_line_reply(p, last, &dst_len);
            break;

        case '$':
            p++;
            dst = parse_bulk_reply(p, last, &dst_len);
            break;

        default:
            goto invalid;
        }

        if (dst_len == -2) {
            dd("bulk %d reply parse fail for multi bulks", i);
            return PARSE_ERROR;
        }

        if (dst_len == -1) {
            lua_pushnil(L);
            p = dst + sizeof("\r\n") - 1;

        } else {
            lua_pushlstring(L, dst, dst_len);
            p = dst + dst_len + sizeof("\r\n") - 1;
        }

        lua_rawseti(L, -2, i);
    }

    *src = (char *) p;

    return PARSE_OK;

invalid:
    return PARSE_ERROR;
}


static int
redis_build_query(lua_State *L)
{
    int          i, n;
    size_t       len, total;
    const char  *p;
    char        *last;
    char        *buf;
    int          flag;

    if (lua_gettop(L) != 1) {
        return luaL_error(L, "expected one argument but got %d",
                lua_gettop(L));
    }

    luaL_checktype(L, 1, LUA_TTABLE);

    n = lua_objlen(L, 1);

    if (n == 0) {
        return luaL_error(L, "empty input param table");
    }

    total = sizeof("*") - 1
          + get_num_size(n)
          + sizeof("\r\n") - 1
          ;

    for (i = 1; i <= n; i++) {
        lua_rawgeti(L, 1, i);

        dd("param type: %d (%d)", lua_type(L, -1), LUA_TUSERDATA);

        switch (lua_type(L, -1)) {
            case LUA_TSTRING:
            case LUA_TNUMBER:
                lua_tolstring(L, -1, &len);

                total += sizeof("$") - 1
                       + get_num_size(len)
                       + sizeof("\r\n") - 1
                       + len
                       + sizeof("\r\n") - 1
                       ;

                break;

            case LUA_TBOOLEAN:
                total += sizeof("$1\r\n1\r\n") - 1;
                break;

            case LUA_TLIGHTUSERDATA:
                p = lua_touserdata(L, -1);
                dd("user data: %p", p);
                if (p == redis_null) {
                    total += sizeof("$-1\r\n") - 1;
                    break;
                }

            default:
                return luaL_error(L, "parameter %d is not a string, number, "
                        "redis.parser.null, or boolean value", i);
        }

        lua_pop(L, 1);
    }

    buf = lua_newuserdata(L, total); /* lua_newuserdata never returns NULL */

    last = buf;

    *last++ = '*';
    last = sprintf_num(last, n);
    *last++ = '\r'; *last++ = '\n';

    for (i = 1; i <= n; i++) {
        lua_rawgeti(L, 1, i);

        switch (lua_type(L, -1)) {
            case LUA_TSTRING:
            case LUA_TNUMBER:
                p = luaL_checklstring(L, -1, &len);

                *last++ = '$';

                last = sprintf_num(last, len);

                *last++ = '\r'; *last++ = '\n';

                memcpy(last, p, len);
                last += len;

                *last++ = '\r'; *last++ = '\n';

                break;

            case LUA_TBOOLEAN:
                memcpy(last, "$1\r\n", sizeof("$1\r\n") - 1);
                last += sizeof("$1\r\n") - 1;

                flag = lua_toboolean(L, -1);
                *last++ = flag ? '1' : '0';

                *last++ = '\r'; *last++ = '\n';

                break;

            case LUA_TLIGHTUSERDATA:
                /* must be null */
                memcpy(last, "$-1\r\n", sizeof("$-1\r\n") - 1);
                last += sizeof("$-1\r\n") - 1;
                break;

            default:
                /* cannot reach here */
                break;
        }

        lua_pop(L, 1);
    }

    if (last - buf != (ssize_t) total) {
        return luaL_error(L, "buffer error");
    }

    lua_pushlstring(L, buf, total);

    return 1;
}


static size_t
get_num_size(size_t i)
{
    size_t          n = 0;

    do {
        i = i / 10;
        n++;
    } while (i > 0);

    return n;
}


static char *
sprintf_num(char *dst, int64_t ui64)
{
    char             *p;
    char              temp[UINT64_LEN + 1];
    size_t            len;

    p = temp + UINT64_LEN;

    do {
        *--p = (char) (ui64 % 10 + '0');
    } while (ui64 /= 10);

    len = (temp + UINT64_LEN) - p;

    memcpy(dst, p, len);

    return dst + len;
}


static int
redis_typename(lua_State *L)
{
    int         typ;

    if (lua_gettop(L) != 1) {
        return luaL_error(L, "expecting one argument but got %d",
                lua_gettop(L));
    }

    typ = (int) luaL_checkinteger(L, 1);

    if (typ < 0 || typ >= redis_nelems(redis_typenames)) {
        lua_pushnil(L);
        return 1;
    }

    lua_pushstring(L, redis_typenames[typ]);
    return 1;
}

