////////////////////////////////////////////////////////////////////
//
// This file contains the code to compare the contents of tables.
// The top level functions in this file are:
//
//  World::diff_numbered_tables
//  World::diff_tangible_databases
//  World::patch_numbered_tables
//  World::patch_tangible_databases
//
// It also contains these unit testing support routines:
//
//  table.diffcompare
//  table.diffapply
//
// This file also contains all the support code needed to implement
// this stuff.
//
////////////////////////////////////////////////////////////////////

#include "luastack.hpp"
#include "streambuffer.hpp"
#include "table.hpp"
#include "world.hpp"
#include <iostream>

// Given a table and an tnmap, return the table number of the table.
// Returns zero if the table doesn't have a table number.
//
static int get_table_number(LuaStack &MLS, LuaSlot mval, LuaSlot mtnmap) {
    lua_State *L = MLS.state();
    lua_pushvalue(L, mval.index());
    lua_rawget(L, mtnmap.index());
    int result = 0;
    if (lua_type(L, -1) == LUA_TNUMBER) {
        result = lua_tointeger(L, -1);
    }
    lua_pop(L, 1);
    return result;
}


static bool equivalent_values(LuaStack &MLS, LuaSlot mval, LuaSlot mtnmap,
                              LuaStack &SLS, LuaSlot sval, LuaSlot stnmap) {
    switch (MLS.xtype(mval)) {
    case LUA_TBOOLEAN: {
        if (SLS.type(sval) != LUA_TBOOLEAN) return false;
        return MLS.ckboolean(mval) == SLS.ckboolean(sval);
    }
    case LUA_TNUMBER: {
        if (SLS.type(sval) != LUA_TNUMBER) return false;
        return MLS.cknumber(mval) == SLS.cknumber(sval);
    }
    case LUA_TSTRING: {
        // This could be faster if I used lua_tolstring directly.
        if (SLS.type(sval) != LUA_TSTRING) return false;
        return MLS.ckstring(mval) == SLS.ckstring(sval);
    }
    case LUA_TFUNCTION: {
        // Cannot really compare.  Just return true if the types match.
        return SLS.type(sval) == MLS.type(mval);
    }
    case LUA_TT_GENERAL: {
        int midx = get_table_number(MLS, mval, mtnmap);
        if (midx == 0) {
            return SLS.isnil(sval);
        }
        int sidx = get_table_number(SLS, sval, stnmap);
        return midx == sidx;
    }
    case LUA_TT_CLASS: {
        if (SLS.xtype(sval) != LUA_TT_CLASS) return false;
        // What if it's an ill-formed class?
        return MLS.classname(mval) == SLS.classname(sval);
    }
    case LUA_TT_TANGIBLE: {
        if (SLS.xtype(sval) != LUA_TT_TANGIBLE) return false;
        // What if it's an ill-formed tangible?
        return MLS.tanid(mval) == SLS.tanid(sval);
    }
    case LUA_TT_GLOBALENV: {
        return (SLS.xtype(sval) == LUA_TT_GLOBALENV);
    }
    default:
        // We're forcing anything else to go to NIL,
        // and once it's NIL, we consider it 'equal'.
        return SLS.type(sval) == LUA_TNIL;
    }
}

static void transmit_value(LuaStack &MLS, LuaSlot mval, LuaSlot mtnmap, StreamBuffer *sb) {
    switch (MLS.xtype(mval)) {
    case LUA_TBOOLEAN: {
        sb->write_uint8(LUA_TBOOLEAN);
        sb->write_bool(MLS.ckboolean(mval));
        return;
    }
    case LUA_TNUMBER: {
        sb->write_uint8(LUA_TNUMBER);
        sb->write_double(MLS.cknumber(mval));
        return;
    }
    case LUA_TSTRING: {
        sb->write_uint8(LUA_TSTRING);
        sb->write_string(MLS.ckstring(mval));
        return;
    }
    case LUA_TT_GENERAL: {
        int midx = get_table_number(MLS, mval, mtnmap);
        if (midx == 0) {
            sb->write_uint8(LUA_TNIL);
        } else {
            sb->write_uint8(LUA_TT_GENERAL);
            sb->write_uint32(midx);
        }
        return;
    }
    case LUA_TT_CLASS: {
        sb->write_uint8(LUA_TT_CLASS);
        sb->write_string(MLS.classname(mval));
        return;
    }
    case LUA_TT_TANGIBLE: {
        sb->write_uint8(LUA_TT_TANGIBLE);
        sb->write_int64(MLS.tanid(mval));
        return;
    }
    case LUA_TT_GLOBALENV: {
        sb->write_uint8(LUA_TT_GLOBALENV);
        return;
    }
    default:
        sb->write_uint8(LUA_TNIL);
        return;
    }
}

static void transmit_value_debug_string(StreamBuffer *sb, eng::ostringstream &oss) {
    int kind = sb->read_uint8();
    switch (kind) {
    case LUA_TBOOLEAN: {
        bool b = sb->read_bool();
        oss << (b ? "true":"false");
        return;
    }
    case LUA_TNUMBER: {
        oss << sb->read_double();
        return;
    }
    case LUA_TSTRING: {
        oss << sb->read_string();
        return;
    }
    case LUA_TT_GENERAL: {
        oss << "table " << sb->read_int32();
        return;
    }
    case LUA_TT_CLASS: {
        oss << "class " << sb->read_string();
        return;
    }
    case LUA_TT_TANGIBLE: {
        oss << "tan " << sb->read_int64();
        return;
    }
    case LUA_TT_GLOBALENV: {
        oss << "globals";
        return;
    }
    case LUA_TNIL: {
        oss << "nil";
        return;
    }
    default:
        assert(false); // Should not get here.
    }
}

static bool diff_tables(LuaStack &SLS0, LuaSlot stnmap, LuaSlot stab,
                        LuaStack &MLS0, LuaSlot mtnmap, LuaSlot mtab,
                        bool cmeta, StreamBuffer *sb) {
    LuaVar skey, mkey, sval, mval, mnil;
    LuaStack SLS(SLS0.state(), skey, sval);
    LuaStack MLS(MLS0.state(), mkey, mval, mnil);
    assert(MLS.istable(mtab));
    assert(SLS.istable(stab));
    MLS.set(mnil, LuaNil);
    int nupdates = 0;

    sb->write_int32(0);
    int wc = sb->total_writes();

    MLS.set(mkey, LuaNil);
    while (MLS.next(mtab, mkey, mval)) {
        if (!MLS.issortablekey(mkey)) continue;
        MLS.movesortablekey(mkey, SLS, skey);
        SLS.rawget(sval, stab, skey);
        if (!equivalent_values(MLS, mval, mtnmap, SLS, sval, stnmap)) {
            transmit_value(MLS, mkey, mtnmap, sb);
            transmit_value(MLS, mval, mtnmap, sb);
            nupdates += 1;
        }
    }

    SLS.set(skey, LuaNil);
    while (SLS.next(stab, skey, sval)) {
        if (!SLS.issortablekey(skey)) continue;
        SLS.movesortablekey(skey, MLS, mkey);
        MLS.rawget(mval, mtab, mkey);
        if (MLS.isnil(mval)) {
            transmit_value(MLS, mkey, mtnmap, sb);
            transmit_value(MLS, mval, mtnmap, sb);
            nupdates += 1;
        }
    }

    if (cmeta) {
        SLS.getmetatable(sval, stab);
        MLS.getmetatable(mval, mtab);
        if (!equivalent_values(MLS, mval, mtnmap, SLS, sval, stnmap)) {
            transmit_value(MLS, mnil, mtnmap, sb);
            transmit_value(MLS, mval, mtnmap, sb);
            nupdates += 1;
        }
    }

    sb->overwrite_int32(wc, nupdates);

    SLS.result();
    MLS.result();
    return (nupdates > 0);
}

static eng::string diff_tables_debug_string(StreamBuffer *sb) {
    eng::vector<eng::string> sorted;
    eng::ostringstream oss;
    int ndiffs = sb->read_int32();
    for (int i = 0; i < ndiffs; i++) {
        transmit_value_debug_string(sb, oss);
        oss << "=";
        transmit_value_debug_string(sb, oss);
        sorted.push_back(oss.str());
        oss.str("");
    }
    std::sort(sorted.begin(), sorted.end());
    for (const eng::string &s : sorted) {
        oss << s << ";";
    }
    return oss.str();
}

static void set_transmitted_value(LuaStack &LS, LuaSlot tangibles, LuaSlot ntmap, LuaSlot target, StreamBuffer *sb, const char *dbinfo, DebugCollector *dbc) {
    int kind = sb->read_uint8();
    switch (kind) {
    case LUA_TBOOLEAN: {
        bool value = sb->read_bool();
        DebugLine(dbc) << dbinfo << (value ? "true" : "false"); 
        LS.set(target, value);
        return;
    }
    case LUA_TNUMBER: {
        double value = sb->read_double();
        DebugLine(dbc) << dbinfo << value;
        LS.set(target, value);
        return;
    }
    case LUA_TSTRING: {
        eng::string value = sb->read_string();
        DebugLine(dbc) << dbinfo << "'" << value << "'";
        LS.set(target, value);
        return;
    }
    case LUA_TT_GENERAL: {
        int index = sb->read_int32();
        DebugLine(dbc) << dbinfo << "table " << index;
        LS.rawget(target, ntmap, index);
        return;
    }
    case LUA_TT_CLASS: {
        eng::string value = sb->read_string();
        DebugLine(dbc) << dbinfo << "class " << value;
        LS.makeclass(target, value);
        return;
    }
    case LUA_TT_TANGIBLE: {
        int64_t id = sb->read_int64();
        DebugLine(dbc) << dbinfo << "tan " << id;
        LS.rawget(target, tangibles, id);
        if (LS.isnil(target)) {
            World *w = World::fetch_global_pointer(LS.state());
            w->tangible_make(LS.state(), id, "nowhere", true);
            lua_replace(LS.state(), target.index());
        }
        return;
    }
    case LUA_TT_GLOBALENV: {
        DebugLine(dbc) << dbinfo << "global env";
        LS.getglobaltable(target);
        return;
    }
    case LUA_TNIL: {
        DebugLine(dbc) << dbinfo << "nil";
        LS.set(target, LuaNil);
        return;
    }
    default:
        assert(false); // Should not get here.
    }
}

static void patch_table(LuaStack &LS0, LuaSlot tangibles, LuaSlot ntmap, LuaSlot tab, StreamBuffer *sb, DebugCollector *dbc) {
    LuaVar key, val;
    LuaStack LS(LS0.state(), key, val);
    int ndiffs = sb->read_int32();
    for (int i = 0; i < ndiffs; i++) {
        set_transmitted_value(LS, tangibles, ntmap, key, sb, "key=", dbc);
        set_transmitted_value(LS, tangibles, ntmap, val, sb, "val=", dbc);
        if (LS.isnil(key)) {
            LS.setmetatable(tab, val);
        } else {
            LS.rawset(tab, key, val);
        }
    }
    LS.result();
}

void World::patch_numbered_tables(StreamBuffer *sb, DebugCollector *dbc) {
    lua_State *L = state();
    LuaVar tangibles, ntmap, tab;
    LuaStack LS(L, tangibles, ntmap, tab);
    LS.rawget(tangibles, LuaRegistry, "tangibles");
    LS.rawget(ntmap, LuaRegistry, "ntmap");
    assert(LS.istable(tangibles));
    assert(LS.istable(ntmap));

    int nmodified = sb->read_int32();
    for (int i = 0; i < nmodified; i++) {
        int index = sb->read_int32();
        LS.rawget(tab, ntmap, index);
        assert(LS.istable(tab));
        DebugHeader(dbc) << "Lua Table " << index << ":";
        patch_table(LS, tangibles, ntmap, tab, sb, dbc);
    }
    LS.result();
}

void World::diff_numbered_tables(lua_State *master, StreamBuffer *sb) {
    lua_State *synch = state();
    LuaVar sntmap, mntmap, stnmap, mtnmap, stab, mtab;
    LuaStack SLS(synch, sntmap, stnmap, stab);
    LuaStack MLS(master, mntmap, mtnmap, mtab);
    SLS.rawget(sntmap, LuaRegistry, "ntmap");
    MLS.rawget(mntmap, LuaRegistry, "ntmap");
    SLS.rawget(stnmap, LuaRegistry, "tnmap");
    MLS.rawget(mtnmap, LuaRegistry, "tnmap");
    int m_ntables = MLS.rawlen(mntmap);
    int s_ntables = SLS.rawlen(sntmap);
    assert(m_ntables == s_ntables);
    
    sb->write_int32(0);
    int write_count_after = sb->total_writes();
    int nmodified = 0;
    int s_top = lua_gettop(synch);
    int m_top = lua_gettop(master);
    for (int id = 1; id <= m_ntables; id++) {
        MLS.rawget(mtab, mntmap, id);
        if (MLS.istable(mtab)) {
            SLS.rawget(stab, sntmap, id);
            assert(SLS.istable(stab));
            int tw = sb->total_writes();
            sb->write_int32(id);
            nmodified += 1;
            if (!diff_tables(SLS, stnmap, stab, MLS, mtnmap, mtab, true, sb)) {
                sb->unwrite_to(tw);
                nmodified -= 1;
            }
        }
        assert(lua_gettop(synch) == s_top);
        assert(lua_gettop(master) == m_top);
    }
    sb->overwrite_int32(write_count_after, nmodified);
    SLS.result();
    MLS.result();
}

void World::patch_tangible_databases(StreamBuffer *sb, DebugCollector *dbc) {
    lua_State *L = state();
    LuaVar tangibles, ntmap, tab;
    LuaStack LS(L, tangibles, ntmap, tab);
    LS.rawget(tangibles, LuaRegistry, "tangibles");
    LS.rawget(ntmap, LuaRegistry, "ntmap");
    assert(LS.istable(tangibles));
    assert(LS.istable(ntmap));

    int nmodified = sb->read_int32();
    for (int i = 0; i < nmodified; i++) {
        int64_t id = sb->read_int64();
        LS.rawget(tab, tangibles, id);
        assert(LS.istable(tab));
        DebugHeader(dbc) << "Tangible DB " << id << ":";
        patch_table(LS, tangibles, ntmap, tab, sb, dbc);
    }
    LS.result();
}

void World::diff_tangible_databases(const IdVector &basis, lua_State *master, StreamBuffer *sb) {
    lua_State *synch = state();
    LuaVar stnmap, mtnmap, stangibles, mtangibles, stab, mtab;
    LuaStack SLS(synch, stnmap, stangibles, stab);
    LuaStack MLS(master, mtnmap, mtangibles, mtab);
    SLS.rawget(stnmap, LuaRegistry, "tnmap");
    MLS.rawget(mtnmap, LuaRegistry, "tnmap");
    SLS.rawget(stangibles, LuaRegistry, "tangibles");
    MLS.rawget(mtangibles, LuaRegistry, "tangibles");
    
    sb->write_int32(0);
    int write_count_after = sb->total_writes();
    int nmodified = 0;
    int s_top = lua_gettop(synch);
    int m_top = lua_gettop(master);
    for (int64_t id : basis) {
        MLS.rawget(mtab, mtangibles, id);
        SLS.rawget(stab, stangibles, id);
        assert(MLS.istable(mtab));
        assert(SLS.istable(stab));
        int tw = sb->total_writes();
        sb->write_int64(id);
        nmodified += 1;
        if (!diff_tables(SLS, stnmap, stab, MLS, mtnmap, mtab, false, sb)) {
            sb->unwrite_to(tw);
            nmodified -= 1;
        }
        assert(lua_gettop(synch) == s_top);
        assert(lua_gettop(master) == m_top);
    }
    sb->overwrite_int32(write_count_after, nmodified);
    SLS.result();
    MLS.result();
}

LuaDefine(table_diffcompare, "mtnmap,mtab,stnmap,stab", "for unit testing only") {
    LuaArg mtnmap, mtab, mstnmap, mstab;
    LuaRet dbgstring;
    LuaVar tthread;
    LuaStack MLS(L, mtnmap, mtab, mstnmap, mstab, dbgstring, tthread);
    // Check the arguments.
    MLS.checktable(mtnmap);
    MLS.checktable(mstnmap);
    MLS.checktable(mtab);
    MLS.checktable(mstab);

    // Create a temporary thread to be the 'synch model'.  We'll use the
    // existing thread as the 'master model'.  Move two tables to the synch thread.
    lua_State *synch = lua_newthread(L);
    lua_replace(L, tthread.index());
    lua_pushvalue(L, mstnmap.index());
    lua_pushvalue(L, mstab.index());
    lua_xmove(L, synch, 2);
    LuaArg stnmap,stab;
    LuaStack SLS(synch, stnmap, stab);

    // Call tablecmp_diff.
    StreamBuffer sb;
    diff_tables(SLS, stnmap, stab, MLS, mtnmap, mtab, true, &sb);

    // Convert the output to a debug string.
    MLS.set(dbgstring, diff_tables_debug_string(&sb));
    return MLS.result();
}

LuaDefine(table_diffapply, "mtnmap,mtab,mstab", "for unit testing only") {
    LuaArg mtnmap, mtab, mstab;
    LuaRet eql, eqlstr, rtab;
    LuaVar tthread, tangibles, mntmap, key, val;
    LuaStack MLS(L, mtnmap, mtab, mstab, eql, eqlstr, rtab, tthread, tangibles, mntmap, key, val);
    // Check the arguments.
    MLS.checktable(mtnmap);
    MLS.checktable(mtab);
    MLS.checktable(mstab);

    // Get the tangibles map.
    MLS.rawget(tangibles, LuaRegistry, "tangibles");

    // Invert the tnmap to make the ntmap.
    MLS.set(mntmap, LuaNewTable);
    MLS.set(key, LuaNil);
    while (MLS.next(mtnmap, key, val)) {
        MLS.rawset(mntmap, val, key);
    }

    // Create a temporary thread to be the 'synch model'.  We'll use the
    // existing thread as the 'master model'.
    lua_State *synch = lua_newthread(L);
    lua_replace(L, tthread.index());
    lua_pushvalue(L, mtnmap.index());
    lua_pushvalue(L, mstab.index());
    lua_xmove(L, synch, 2);
    LuaArg stnmap, stab;
    LuaStack SLS(synch, stnmap, stab);

    // Call diff_tables and patch_tables
    StreamBuffer sb;
    diff_tables(SLS, stnmap, stab, MLS, mtnmap, mtab, true, &sb);

    patch_table(MLS, tangibles, mntmap, mstab, &sb, nullptr);

    bool eq = table_equal(MLS, mstab, mtab);
    MLS.set(eql, eq);
    if (eq) {
        MLS.set(eqlstr, "tables equal");
    } else {
        MLS.set(eqlstr, "tables were supposed to be equal");
    }
    MLS.set(rtab, stab);

    return MLS.result();
}


