#include "serializelua.hpp"

enum PackCodes {
    LUA_PK_TRUE = LUA_TT_SENTINEL,
    LUA_PK_FALSE,
    LUA_PK_REFERENCE,
    LUA_PK_ENDTABLE
};

class DeserializeError {
};

class Deserializer {
    LuaVar id_to_value_;
    LuaExtStack LS_;
    eng::string &error_;
    StreamBuffer *sb_;
    int next_id_;

    void deserialize_table_r(LuaSlot target) {
        LuaVar key, val;
        LuaExtStack LS(LS_.state(), key, val);
        LS.newtable(target);
        LS.rawset(id_to_value_, next_id_++, target);
        bool hasmeta = sb_->read_bool();
        if (hasmeta) {
            uint8_t vtype = sb_->read_uint8();
            deserialize_r(vtype, val);
            if (!LS.istable(val)) {
                error_ = "serialized data contains invalid metatable";
                throw DeserializeError();
            }
            LS.setmetatable(target, val);
        }
        while (true) {
            uint8_t ktype = sb_->read_uint8();
            if (ktype == LUA_PK_ENDTABLE) {
                break;
            }
            deserialize_r(ktype, key);
            uint8_t vtype = sb_->read_uint8();
            deserialize_r(vtype, val);
            LS.rawset(target, key, val);
        }
    }

    void deserialize_r(uint8_t kind, LuaSlot target) {
        switch (kind) {
            case LUA_TNIL: {
                LS_.set(target, LuaNil);
                return;
            }
            case LUA_PK_TRUE: {
                LS_.set(target, true);
                return;
            }
            case LUA_PK_FALSE: {
                LS_.set(target, false);
                return;
            }
            case LUA_TLIGHTUSERDATA: {
                LS_.set(target, LuaToken(sb_->read_uint64()));
                return;
            }
            case LUA_TNUMBER: {
                LS_.set(target, sb_->read_double());
                return;
            }
            case LUA_TSTRING: {
                LS_.set(target, sb_->read_string());
                LS_.rawset(id_to_value_, next_id_++, target);
                return;
            }
            case LUA_TT_GENERAL: {
                deserialize_table_r(target);
                return;
            }
            case LUA_TT_GLOBALENV: {
                LS_.getglobaltable(target);
                return;
            }
            case LUA_TT_TANGIBLE: {
                int64_t tanid = sb_->read_int64();
                if (!LS_.validpositiveinteger(tanid)) {
                    throw DeserializeError();
                }
                LS_.maketan(target, tanid);
                return;
            }
            case LUA_TT_CLASS: {
                eng::string name = sb_->read_string();
                if (!sv::is_lua_classname(name)) {
                    throw DeserializeError();
                }
                LS_.makeclass(target, name);
                return;
            }
            case LUA_PK_REFERENCE: {
                int32_t key = sb_->read_int32();
                LS_.rawget(target, id_to_value_, key);
                if (LS_.isnil(target)) {
                    error_ = "invalid backward reference in serialized data";
                    throw DeserializeError();
                }
                return;
            }
            default: {
                error_ = util::ss("unrecognized type token in serialized data: ", kind);
                throw DeserializeError();
            }
        }
    }

public:
    Deserializer(LuaCoreStack &LS0, LuaSlot val, StreamBuffer *sb, eng::string &error) :
        LS_(LS0.state(), id_to_value_), error_(error), sb_(sb), next_id_(1) {
        LS_.newtable(id_to_value_);
        int top = lua_gettop(LS_.state());
        try {
            uint16_t v = sb_->read_uint16();
            if (v != 0xD096) {
                error_ = "This is not a serialized data structure";
                return;
            }
            uint8_t b = sb_->read_uint8();
            deserialize_r(b, val);
        } catch (const StreamException &e) {
            error_ = "EOF reached while deserializing data";
            lua_settop(LS_.state(), top);
            LS_.set(val, LuaNil);
        } catch (DeserializeError e) {
            lua_settop(LS_.state(), top);
            LS_.set(val, LuaNil);
        }
    }
};


// If the serialize routine encounters an error, it must set the
// error message string.  In that case, it is assumed that the contents
// of the serialization buffer is no longer valid and there is no
// requirement to try to keep it valid.

class Serializer {
    LuaVar lookup_;
    LuaVar value_to_id_;
    LuaExtStack LS_;
    eng::string &error_;
    StreamBuffer *sb_;
    int next_id_;

    void serialize_table_r(LuaSlot tab) {
        LuaVar key, val;
        LuaExtStack SLS(LS_.state(), key, val);
        sb_->write_uint8(LUA_TT_GENERAL);
        SLS.getmetatable(val, tab);
        if (SLS.isnil(val)) {
            sb_->write_bool(false);
        } else {
            sb_->write_bool(true);
            serialize_r(val);
        }
        SLS.set(key, LuaNil);
        while (SLS.next(tab, key, val)) {
            serialize_r(key);
            if (!error_.empty()) {
                return;
            }
            serialize_r(val);
            if (!error_.empty()) {
                return;
            }
        }
        sb_->write_uint8(LUA_PK_ENDTABLE);
    }

    void serialize_r(LuaSlot val) {
        int tt = LS_.xtype(val);
        switch (tt) {
            case LUA_TNIL: {
                sb_->write_uint8(LUA_TNIL);
                return;
            }
            case LUA_TBOOLEAN: {
                if (LS_.ckboolean(val)) {
                    sb_->write_uint8(LUA_PK_TRUE);
                } else {
                    sb_->write_uint8(LUA_PK_FALSE);
                }
                return;
            }
            case LUA_TLIGHTUSERDATA: {
                sb_->write_uint8(LUA_TLIGHTUSERDATA);
                sb_->write_uint64(LS_.cktoken(val).value);
                return;
            }
            case LUA_TNUMBER: {
                sb_->write_uint8(LUA_TNUMBER);
                sb_->write_double(LS_.cknumber(val));
                return;
            }
            case LUA_TSTRING: {
                LS_.rawget(lookup_, value_to_id_, val);
                if (!LS_.isnil(lookup_)) {
                    sb_->write_uint8(LUA_PK_REFERENCE);
                    sb_->write_int32(LS_.ckint(lookup_));
                } else {
                    LS_.rawset(value_to_id_, val, next_id_++);
                    sb_->write_uint8(LUA_TSTRING);
                    sb_->write_string(LS_.ckstring(val));
                }
                return;
            }
            case LUA_TTABLE: {
                // LS.xtype should never return LUA_TTABLE.
                error_ = "Bad xtype in serialization";
                return;
            }
            case LUA_TFUNCTION: {
                error_ = "Cannot serialize closures";
                return;
            }
            case LUA_TUSERDATA: {
                error_ = "Cannot serialize userdata";
                return;
            }
            case LUA_TTHREAD: {
                error_ = "Cannot serialize coroutines";
                return;
            }
            case LUA_TT_GENERAL: {
                LS_.rawget(lookup_, value_to_id_, val);
                if (!LS_.isnil(lookup_)) {
                    sb_->write_uint8(LUA_PK_REFERENCE);
                    sb_->write_int32(LS_.ckint(lookup_));
                } else {
                    LS_.rawset(value_to_id_, val, next_id_++);
                    serialize_table_r(val);
                }
                return;
            }
            case LUA_TT_REGISTRY: {
                error_ = "Pointer to registry found in serialization, shouldn't happen";
                return;
            }
            case LUA_TT_GLOBALENV: {
                sb_->write_uint8(LUA_TT_GLOBALENV);
                return;
            }
            case LUA_TT_TANGIBLE: {
                sb_->write_uint8(LUA_TT_TANGIBLE);
                sb_->write_int64(LS_.tanid(val));
                return;
            }
            case LUA_TT_TANGIBLEMETA: {
                error_ = "Pointer to a tangible metatable found in serialization, shouldn't happen";
                return;
            }
            case LUA_TT_CLASS: {
                sb_->write_uint8(LUA_TT_CLASS);
                sb_->write_string(LS_.classname(val));
                return;
            }
            default: {
                error_ = "Unrecognized xtype in serialization";
                return;
            }
        }
    }

public:
    Serializer(LuaCoreStack &LS0, LuaSlot val, StreamBuffer *sb, eng::string &error) :
        LS_(LS0.state(), lookup_, value_to_id_), error_(error), sb_(sb), next_id_(1) {
        LS_.newtable(value_to_id_);
        sb_->write_uint16(0xD096);
        serialize_r(val);
    }
};

eng::string serialize_lua(LuaCoreStack &LS0, LuaSlot val, StreamBuffer *sb) {
    eng::string error;
    Serializer sz(LS0, val, sb, error);
    return error;
};

eng::string deserialize_lua(LuaCoreStack &LS0, LuaSlot val, StreamBuffer *sb) {
    eng::string error;
    Deserializer dsz(LS0, val, sb, error);
    if (!error.empty()) {
        LS0.set(val, LuaNil);
    }
    return error;
}

LuaDefine(table_serialize, "value",
    "|Serialize any lua value, returning a string"
    "|"
    "|Converts lua values into a binary string. The string"
    "|can then be deserialized to produce a copy of the value."
    "|"
    "|Supports these atomic types:"
    "|"
    "|  nil, string, number, boolean, token"
    "|"
    "|Does not support these types at all:"
    "|"
    "|  function, thread"
    "|"
    "|Supports simple tables.  When it finds a simple table, it recurses"
    "|into the table making a deep copy. Metatables are also supported."
    "|"
    "|Cycles are supported: if you have two tables which point to each other,"
    "|they can be serialized, and the deserialized copy will contain two"
    "|tables that point to each other."
    "|"
    "|Duplicate tables and duplicate strings are handled efficiently."
    "|For example, if the data contains two copies of a 10KB string, the"
    "|serialized data will only contain one copy of the string."
    "|"
    "|Tangibles are tables, but they are handled specially.  When the"
    "|serialize routine finds a tangible, it just stores the tangible's ID."
    "|The deserialized copy will then contain the same tangible by ID."
    "|"
    "|Classes are also tables, but they too are handled specially.  When"
    "|the serialize routine finds a class, it stores the class name, and"
    "|the deserialized copy will then contain the same class."
    "|"
    "|The global environment table is also treated specially: if you"
    "|serialize it, it will not recurse into it, instead, the deserialized"
    "|copy will just contain a pointer to the global environment."
    "|") {
    LuaArg value;
    LuaRet str;
    LuaDefStack LS(L, value, str);
    StreamBuffer sb;
    eng::string error = serialize_lua(LS, value, &sb);
    if (!error.empty()) {
        luaL_error(L, "%s", error.c_str());
        return LS.result();
    }
    LS.set(str, sb.view());
    return LS.result();
}

LuaDefine(table_deserialize, "binary",
    "|Deserialize a serialized block, returning a value"
    "|"
    "|See doc(table.serialize) for more information about what"
    "|kind of data can be serialized and how it is deserialized."
    "|") {
    LuaArg str;
    LuaRet value;
    LuaDefStack LS(L, value, str);
    std::string_view s = LS.ckstringview(str);
    StreamBuffer sb(s);
    eng::string error = deserialize_lua(LS, value, &sb);
    if (!error.empty()) {
        luaL_error(L, "%s", error.c_str());
        return LS.result();
    }
    return LS.result();
}