/*
 * lmarshal.c
 * A Lua library for serializing and deserializing Lua values
 * Richard Hundt <richardhundt@gmail.com>, Eluna Lua Engine <http://emudevs.com/>
 *
 * License: MIT
 *
 * Copyright (c) 2010 Richard Hundt
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */

#include <stdlib.h>
#include <string.h>
#include <cstdint>
#include "ElunaCompat.h"

#if LUA_VERSION_NUM == 501 && !defined(luaL_setfuncs)
    #define luaL_setfuncs(L, l, n) luaL_register(L, NULL, l)
#endif

#define MAR_TREF 1
#define MAR_TVAL 2
#define MAR_TUSR 3

#define MAR_CHR 1
#define MAR_I32 4
#define MAR_I64 8

#define MAR_MAGIC 0x8f
#define SEEN_IDX  3

#define MAR_ENV_IDX_KEY  "E"
#define MAR_NUPS_IDX_KEY "n"

typedef struct mar_Buffer {
    size_t size;
    size_t seek;
    size_t head;
    char*  data;
} mar_Buffer;

static int mar_encode_table(lua_State *L, mar_Buffer *buf, size_t *idx);
static int mar_decode_table(lua_State *L, const char* buf, size_t len, size_t *idx);

static void buf_init(lua_State *L, mar_Buffer *buf)
{
    buf->size = 128;
    buf->seek = 0;
    buf->head = 0;
    if (!(buf->data = (char*)malloc(buf->size))) luaL_error(L, "Out of memory!");
}

static void buf_done(lua_State* /*L*/, mar_Buffer *buf)
{
    free(buf->data);
}

static int buf_write(lua_State* L, const char* str, size_t len, mar_Buffer *buf)
{
    if (len > UINT32_MAX) luaL_error(L, "buffer too long");
    if (buf->size - buf->head < len) {
        size_t new_size = buf->size << 1;
        size_t cur_head = buf->head;
        while (new_size - cur_head <= len) {
            new_size = new_size << 1;
        }
        char* data = (char*)realloc(buf->data, new_size);
        if (!data) {
            return luaL_error(L, "Out of memory!");
        }
        buf->data = data;
        buf->size = new_size;
    }
    memcpy(&buf->data[buf->head], str, len);
    buf->head += len;
    return 0;
}

static const char* buf_read(lua_State* /*L*/, mar_Buffer *buf, size_t *len)
{
    if (buf->seek < buf->head) {
        buf->seek = buf->head;
        *len = buf->seek;
        return buf->data;
    }
    *len = 0;
    return NULL;
}

static void mar_encode_value(lua_State *L, mar_Buffer *buf, int val, size_t *idx)
{
    size_t l;
    int val_type = lua_type(L, val);
    lua_pushvalue(L, val);

    buf_write(L, (const char*)&val_type, MAR_CHR, buf);
    switch (val_type) {
    case LUA_TBOOLEAN: {
        int int_val = lua_toboolean(L, -1);
        buf_write(L, (const char*)&int_val, MAR_CHR, buf);
        break;
    }
    case LUA_TSTRING: {
        const char *str_val = lua_tolstring(L, -1, &l);
        buf_write(L, (const char*)&l, MAR_I32, buf);
        buf_write(L, str_val, l, buf);
        break;
    }
    case LUA_TNUMBER: {
        lua_Number num_val = lua_tonumber(L, -1);
        buf_write(L, (const char*)&num_val, MAR_I64, buf);
        break;
    }
    case LUA_TTABLE: {
        int tag, ref;
        lua_pushvalue(L, -1);
        lua_rawget(L, SEEN_IDX);
        if (!lua_isnil(L, -1)) {
            ref = lua_tointeger(L, -1);
            tag = MAR_TREF;
            buf_write(L, (const char*)&tag, MAR_CHR, buf);
            buf_write(L, (const char*)&ref, MAR_I32, buf);
            lua_pop(L, 1);
        }
        else {
            mar_Buffer rec_buf;
            lua_pop(L, 1); /* pop nil */
            if (luaL_getmetafield(L, -1, "__persist")) {
                tag = MAR_TUSR;

                lua_pushvalue(L, -2); /* self */
                lua_call(L, 1, 1);
                if (!lua_isfunction(L, -1)) {
                    luaL_error(L, "__persist must return a function");
                }

                lua_remove(L, -2); /* __persist */

                lua_newtable(L);
                lua_pushvalue(L, -2); /* callback */
                lua_rawseti(L, -2, 1);

                buf_init(L, &rec_buf);
                mar_encode_table(L, &rec_buf, idx);

                buf_write(L, (const char*)&tag, MAR_CHR, buf);
                buf_write(L, (const char*)&rec_buf.head, MAR_I32, buf);
                buf_write(L, rec_buf.data, rec_buf.head, buf);
                buf_done(L, &rec_buf);
                lua_pop(L, 1);
            }
            else {
                tag = MAR_TVAL;

                lua_pushvalue(L, -1);
                lua_pushinteger(L, (*idx)++);
                lua_rawset(L, SEEN_IDX);

                lua_pushvalue(L, -1);
                buf_init(L, &rec_buf);
                mar_encode_table(L, &rec_buf, idx);
                lua_pop(L, 1);

                buf_write(L, (const char*)&tag, MAR_CHR, buf);
                buf_write(L, (const char*)&rec_buf.head, MAR_I32, buf);
                buf_write(L, rec_buf.data,rec_buf.head, buf);
                buf_done(L, &rec_buf);
            }
        }
        break;
    }
    case LUA_TFUNCTION: {
        int tag, ref;
        lua_pushvalue(L, -1);
        lua_rawget(L, SEEN_IDX);
        if (!lua_isnil(L, -1)) {
            ref = lua_tointeger(L, -1);
            tag = MAR_TREF;
            buf_write(L, (const char*)&tag, MAR_CHR, buf);
            buf_write(L, (const char*)&ref, MAR_I32, buf);
            lua_pop(L, 1);
        }
        else {
            mar_Buffer rec_buf;
            unsigned char i;
            lua_Debug ar;
            lua_pop(L, 1); /* pop nil */

            lua_pushvalue(L, -1);
            lua_getinfo(L, ">nuS", &ar);
            if (ar.what[0] != 'L') {
                luaL_error(L, "attempt to persist a C function '%s'", ar.name);
            }
            tag = MAR_TVAL;
            lua_pushvalue(L, -1);
            lua_pushinteger(L, (*idx)++);
            lua_rawset(L, SEEN_IDX);

            lua_pushvalue(L, -1);
            buf_init(L, &rec_buf);
            lua_dump(L, (lua_Writer)buf_write, &rec_buf);

            buf_write(L, (const char*)&tag, MAR_CHR, buf);
            buf_write(L, (const char*)&rec_buf.head, MAR_I32, buf);
            buf_write(L, rec_buf.data, rec_buf.head, buf);
            buf_done(L, &rec_buf);
            lua_pop(L, 1);

            lua_createtable(L, ar.nups, 0);
            for (i = 1; i <= ar.nups; i++) {
                const char* upvalue_name = lua_getupvalue(L, -2, i);
                if (strcmp("_ENV", upvalue_name) == 0) {
                    lua_pop(L, 1);
                    // Mark where _ENV is expected.
                    lua_pushstring(L, MAR_ENV_IDX_KEY);
                    lua_pushinteger(L, i);
                    lua_rawset(L, -3);
                }
                else {
                    lua_rawseti(L, -2, i);
                }
            }
            lua_pushstring(L, MAR_NUPS_IDX_KEY);
            lua_pushnumber(L, ar.nups);
            lua_rawset(L, -3);

            buf_init(L, &rec_buf);
            mar_encode_table(L, &rec_buf, idx);

            buf_write(L, (const char*)&rec_buf.head, MAR_I32, buf);
            buf_write(L, rec_buf.data, rec_buf.head, buf);
            buf_done(L, &rec_buf);
            lua_pop(L, 1);
        }

        break;
    }
    case LUA_TUSERDATA: {
        int tag, ref;
        lua_pushvalue(L, -1);
        lua_rawget(L, SEEN_IDX);
        if (!lua_isnil(L, -1)) {
            ref = lua_tointeger(L, -1);
            tag = MAR_TREF;
            buf_write(L, (const char*)&tag, MAR_CHR, buf);
            buf_write(L, (const char*)&ref, MAR_I32, buf);
            lua_pop(L, 1);
        }
        else {
            mar_Buffer rec_buf;
            lua_pop(L, 1); /* pop nil */
            if (luaL_getmetafield(L, -1, "__persist")) {
                tag = MAR_TUSR;

                lua_pushvalue(L, -2);
                lua_pushinteger(L, (*idx)++);
                lua_rawset(L, SEEN_IDX);

                lua_pushvalue(L, -2);
                lua_call(L, 1, 1);
                if (!lua_isfunction(L, -1)) {
                    luaL_error(L, "__persist must return a function");
                }
                lua_newtable(L);
                lua_pushvalue(L, -2);
                lua_rawseti(L, -2, 1);
                lua_remove(L, -2);

                buf_init(L, &rec_buf);
                mar_encode_table(L, &rec_buf, idx);

                buf_write(L, (const char*)&tag, MAR_CHR, buf);
                buf_write(L, (const char*)&rec_buf.head, MAR_I32, buf);
		        buf_write(L, rec_buf.data, rec_buf.head, buf);
		        buf_done(L, &rec_buf);
            }
            else {
                luaL_error(L, "attempt to encode userdata (no __persist hook)");
            }
            lua_pop(L, 1);
        }
        break;
    }
    case LUA_TNIL: break;
    default:
        luaL_error(L, "invalid value type (%s)", lua_typename(L, val_type));
    }
    lua_pop(L, 1);
}

static int mar_encode_table(lua_State *L, mar_Buffer *buf, size_t *idx)
{
    lua_pushnil(L);
    while (lua_next(L, -2) != 0) {
        mar_encode_value(L, buf, -2, idx);
        mar_encode_value(L, buf, -1, idx);
        lua_pop(L, 1);
    }
    return 1;
}

#define mar_incr_ptr(l) \
    if (((*p)-buf)+(ptrdiff_t)(l) > (ptrdiff_t)len) luaL_error(L, "bad code"); (*p) += (l);

#define mar_next_len(l,T) \
    if (((*p)-buf)+(ptrdiff_t)sizeof(T) > (ptrdiff_t)len) luaL_error(L, "bad code"); \
    l = *(T*)*p; (*p) += sizeof(T);

static void mar_decode_value
    (lua_State *L, const char *buf, size_t len, const char **p, size_t *idx)
{
    size_t l;
    char val_type = **p;
    mar_incr_ptr(MAR_CHR);
    switch (val_type) {
    case LUA_TBOOLEAN:
        lua_pushboolean(L, *(char*)*p);
        mar_incr_ptr(MAR_CHR);
        break;
    case LUA_TNUMBER:
        lua_pushnumber(L, *(lua_Number*)*p);
        mar_incr_ptr(MAR_I64);
        break;
    case LUA_TSTRING:
        mar_next_len(l, uint32_t);
        lua_pushlstring(L, *p, l);
        mar_incr_ptr(l);
        break;
    case LUA_TTABLE: {
        char tag = *(char*)*p;
        mar_incr_ptr(MAR_CHR);
        if (tag == MAR_TREF) {
            int ref;
            mar_next_len(ref, int);
            lua_rawgeti(L, SEEN_IDX, ref);
        }
        else if (tag == MAR_TVAL) {
            mar_next_len(l, uint32_t);
            lua_newtable(L);
            lua_pushvalue(L, -1);
            lua_rawseti(L, SEEN_IDX, (*idx)++);
            mar_decode_table(L, *p, l, idx);
            mar_incr_ptr(l);
        }
        else if (tag == MAR_TUSR) {
            mar_next_len(l, uint32_t);
            lua_newtable(L);
            mar_decode_table(L, *p, l, idx);
            lua_rawgeti(L, -1, 1);
            lua_call(L, 0, 1);
            lua_remove(L, -2);
            lua_pushvalue(L, -1);
            lua_rawseti(L, SEEN_IDX, (*idx)++);
            mar_incr_ptr(l);
        }
        else {
            luaL_error(L, "bad encoded data");
        }
        break;
    }
    case LUA_TFUNCTION: {
        unsigned int nups;
        unsigned int i;
        mar_Buffer dec_buf;
        char tag = *(char*)*p;
        mar_incr_ptr(1);
        if (tag == MAR_TREF) {
            int ref;
            mar_next_len(ref, int);
            lua_rawgeti(L, SEEN_IDX, ref);
        }
        else {
            mar_next_len(l, uint32_t);
            dec_buf.data = (char*)*p;
            dec_buf.size = l;
            dec_buf.head = l;
            dec_buf.seek = 0;
            lua_load(L, (lua_Reader)buf_read, &dec_buf, "=marshal", NULL);
            mar_incr_ptr(l);

            lua_pushvalue(L, -1);
            lua_rawseti(L, SEEN_IDX, (*idx)++);

            mar_next_len(l, uint32_t);
            lua_newtable(L);
            mar_decode_table(L, *p, l, idx);

            lua_pushstring(L, MAR_ENV_IDX_KEY);
            lua_rawget(L, -2);
            if (lua_isnumber(L, -1)) {
                lua_pushglobaltable(L);
                lua_rawset(L, -3);
            }
            else {
                lua_pop(L, 1);
            }

            lua_pushstring(L, MAR_NUPS_IDX_KEY);
            lua_rawget(L, -2);
            nups = luaL_checknumber(L, -1);
            lua_pop(L, 1);

            for (i = 1; i <= nups; i++) {
                lua_rawgeti(L, -1, i);
                lua_setupvalue(L, -3, i);
            }

            lua_pop(L, 1);
            mar_incr_ptr(l);
        }
        break;
    }
    case LUA_TUSERDATA: {
        char tag = *(char*)*p;
        mar_incr_ptr(MAR_CHR);
        if (tag == MAR_TREF) {
            int ref;
            mar_next_len(ref, int);
            lua_rawgeti(L, SEEN_IDX, ref);
        }
        else if (tag == MAR_TUSR) {
            mar_next_len(l, uint32_t);
            lua_newtable(L);
            mar_decode_table(L, *p, l, idx);
            lua_rawgeti(L, -1, 1);
            lua_call(L, 0, 1);
            lua_remove(L, -2);
            lua_pushvalue(L, -1);
            lua_rawseti(L, SEEN_IDX, (*idx)++);
            mar_incr_ptr(l);
        }
        else { /* tag == MAR_TVAL */
            lua_pushnil(L);
        }
        break;
    }
    case LUA_TNIL:
    case LUA_TTHREAD:
        lua_pushnil(L);
        break;
    default:
        luaL_error(L, "bad code");
    }
}

static int mar_decode_table(lua_State *L, const char* buf, size_t len, size_t *idx)
{
    const char* p;
    p = buf;
    while (p - buf < (ptrdiff_t)len) {
        mar_decode_value(L, buf, len, &p, idx);
        mar_decode_value(L, buf, len, &p, idx);
        lua_rawset(L, -3);
    }
    return 1;
}

int mar_encode(lua_State* L)
{
    const unsigned char m = MAR_MAGIC;
    size_t idx, len;
    mar_Buffer buf;

    if (lua_isnone(L, 1)) {
        lua_pushnil(L);
    }
    if (lua_isnoneornil(L, 2)) {
        lua_newtable(L);
    }
    else if (!lua_istable(L, 2)) {
        luaL_error(L, "bad argument #2 to encode (expected table)");
    }
    lua_settop(L, 2);

    len = lua_rawlen(L, 2);
    lua_newtable(L);
    for (idx = 1; idx <= len; idx++) {
        lua_rawgeti(L, 2, idx);
        if (lua_isnil(L, -1)) {
            lua_pop(L, 1);
            continue;
        }
        lua_pushinteger(L, idx);
        lua_rawset(L, SEEN_IDX);
    }
    lua_pushvalue(L, 1);

    buf_init(L, &buf);
    buf_write(L, (const char*)&m, 1, &buf);

    mar_encode_value(L, &buf, -1, &idx);

    lua_pop(L, 1);

    lua_pushlstring(L, buf.data, buf.head);

    buf_done(L, &buf);

    lua_remove(L, SEEN_IDX);

    return 1;
}

int mar_decode(lua_State* L)
{
    size_t l, idx, len;
    const char *p;
    const char *s = luaL_checklstring(L, 1, &l);

    if (l < 1) luaL_error(L, "bad header");
    if (*(unsigned char *)s++ != MAR_MAGIC) luaL_error(L, "bad magic");
    l -= 1;

    if (lua_isnoneornil(L, 2)) {
        lua_newtable(L);
    }
    else if (!lua_istable(L, 2)) {
        luaL_error(L, "bad argument #2 to decode (expected table)");
    }
    lua_settop(L, 2);

    len = lua_rawlen(L, 2);
    lua_newtable(L);
    for (idx = 1; idx <= len; idx++) {
        lua_rawgeti(L, 2, idx);
        lua_rawseti(L, SEEN_IDX, idx);
    }

    p = s;
    mar_decode_value(L, s, l, &p, &idx);

    lua_remove(L, SEEN_IDX);
    lua_remove(L, 2);

    return 1;
}

int mar_clone(lua_State* L)
{
    mar_encode(L);
    lua_replace(L, 1);
    mar_decode(L);
    return 1;
}

static const luaL_Reg R[] =
{
    {"encode",      mar_encode},
    {"decode",      mar_decode},
    {"clone",       mar_clone},
    {NULL,	    NULL}
};

int luaopen_marshal(lua_State *L)
{
    lua_newtable(L);
    luaL_setfuncs(L, R, 0);
    return 1;
}

