
#include "world.hpp"
#include "pprint.hpp"

static void tangible_getall(LuaStack &LS0, LuaSlot list, const util::IdVector &idv) {
    LuaVar tangibles, tan;
    LuaStack LS(LS0.state(), tangibles, tan);
    LS.rawget(tangibles, LuaRegistry, "tangibles");
    assert(LS.istable(tangibles));
    LS.set(list, LuaNewTable);
    int index = 1;
    for (int64_t id : idv) {
        LS.rawget(tan, tangibles, id);
        assert(LS.istable(tan));
        LS.rawset(list, index++, tan);
    }
    LS.result();
}

LuaDefine(tangible_animstate, "tan", 
    "|Get the entire animation state of the tangible."
    "|Returns six values: graphic,plane,x,y,z,facing.") {
    LuaArg tanobj;
    LuaRet graphic, plane, x, y, z, facing;
    LuaStack LS(L, tanobj, graphic, plane, x, y, z, facing);
    World *w = World::fetch_global_pointer(L);
    Tangible *tan = w->tangible_get(LS, tanobj);
    const AnimStep &aqback = tan->anim_queue_.back();
    LS.set(graphic, aqback.graphic());
    LS.set(plane, aqback.plane());
    LS.set(x, aqback.xyz().x);
    LS.set(y, aqback.xyz().y);
    LS.set(z, aqback.xyz().z);
    LS.set(facing, aqback.facing());
    return LS.result();
}

LuaDefine(tangible_xyz, "tan", 
    "|Get the current coordinates of the tangible."
    "|Returns three values: x, y, z") {
    LuaArg tanobj;
    LuaRet x, y, z;
    LuaStack LS(L, tanobj, x, y, z);
    World *w = World::fetch_global_pointer(L);
    Tangible *tan = w->tangible_get(LS, tanobj);
    const AnimStep &aqback = tan->anim_queue_.back();
    LS.set(x, aqback.xyz().x);
    LS.set(y, aqback.xyz().y);
    LS.set(z, aqback.xyz().z);
    return LS.result();
}

LuaDefine(tangible_animate, "tan,configtable",
    "|Add an animation step to the tangible."
    "|The configtable is a table containing any of the following:"
    "|action,graphic,plane,x,y,z,facing") {
    LuaArg tanobj, config;
    LuaStack LS(L, tanobj, config);
    World *w = World::fetch_global_pointer(L);
    Tangible *tan = w->tangible_get(LS, tanobj);
    int64_t id = w->alloc_id_predictable();
    const AnimStep &prev = tan->anim_queue_.back();
    AnimStep step;
    step.from_lua(L, config.index(), false, prev);
    if (step.action() == "") {
        luaL_error(L, "animation action must be specified");
    }
    tan->anim_queue_.add(id, step);
    tan->update_plane_item();
    return LS.result();
}

LuaDefine(tangible_setclass, "tan,class",
    "|Set the class of the tangible."
    "|The class can be a 'class table' (ie, a table of methods), "
    "|or it can be a string that names a class.  The tangible is "
    "|given an __index metamethod that points at the class table.") {
    LuaArg tanobj, classname;
    LuaVar classtab, mt;
    LuaStack LS(L, tanobj, classname, classtab, mt);
    World *w = World::fetch_global_pointer(L);
    w->tangible_get(LS, tanobj);
    eng::string err = LS.getclass(classtab, classname);
    if (err != "") {
        luaL_error(L, "%s", err.c_str());
    }
    LS.getmetatable(mt, tanobj);
    LS.rawset(mt, "__index", classtab);
    return LS.result();
}

LuaDefine(tangible_getclass, "tan", 
    "|Get the class of the tangible, if any."
    "|The return value is a string, the class name, not"
    "|the class table.") {
    LuaArg tanobj;
    LuaVar mt, classtab;
    LuaRet classname;
    LuaStack LS(L, tanobj, mt, classtab, classname);
    World *w = World::fetch_global_pointer(L);
    w->tangible_get(LS, tanobj);
    LS.getmetatable(mt, tanobj);
    LS.rawget(classtab, mt, "__index");
    eng::string name = LS.classname(classtab);
    if (name == "") {
        LS.set(classname, LuaNil);
    } else {
        LS.set(classname, name);
    }
    return LS.result();
}

LuaDefine(tangible_delete, "tan", 
    "|Delete the specified tangible."
    "|This cannot be used to delete player tangibles,"
    "|To delete a player, use tangible.redirect") {
    LuaArg tanobj;
    LuaStack LS(L, tanobj);
    World *w = World::fetch_global_pointer(L);
    Tangible *tan = w->tangible_get(LS, tanobj);
    assert(tan != nullptr); // this should be checked above.
    if (tan->is_an_actor()) {
        luaL_error(L, "Cannot delete a player using tangible.delete, use tangible.redirect instead.");
        return 0;
    }
    w->tangible_delete(tan->id());
    return LS.result();
}

LuaDefine(tangible_build, "configtable", 
    "|Build a new tangible object."
    "|The configtable must contain: class,x,y,z,plane,graphic."
    "|The configtable can optionally contain: facing.") {
    LuaArg config;
    LuaVar classname, classtab, mt;
    LuaRet database;
    LuaStack LS(L, config, classname, classtab, database, mt);
    
    LS.checktable(config);
    // Get the class of the new tangible.
    LS.rawget(classname, config, "class");
    eng::string err = LS.getclass(classtab, classname);
    if (err != "") {
        luaL_error(L, "%s", err.c_str());
    }

    // Parse the initial animation step.
    AnimStep initstep, blank;
    initstep.from_lua(L, config.index(), true, blank);
    if (!initstep.has_xyz()) {
        luaL_error(L, "You must specify (X,Y,Z) for new tangible");
    }
    if (!initstep.has_plane()) {
        luaL_error(L, "You must specify plane for new tangible");
    }
    if (!initstep.has_graphic()) {
        luaL_error(L, "You must specify graphic for new tangible");
    }

    // TODO: generate error if there's extra crap in the config table.

    World *w = World::fetch_global_pointer(L);
    int64_t new_id = w->alloc_id_predictable();
    Tangible *tan = w->tangible_make(L, new_id, "nowhere", true);
    lua_replace(L, database.index());

    // Update the class of the new tangible.
    LS.getmetatable(mt, database);
    LS.rawset(mt, "__index", classtab);

    // Update the animation queue and planemap of the new tangible.
    int64_t stepid = w->alloc_id_predictable();
    tan->anim_queue_.add(stepid, initstep);
    tan->update_plane_item();

    return LS.result();
}

LuaDefine(tangible_get, "id", 
    "|Get the tangible with the specified id."
    "|This is for debugging only and will be removed in"
    "|the released version.") {
    LuaArg id;
    LuaVar tangibles;
    LuaRet database;
    LuaStack LS(L, id, tangibles, database);
    int64_t nid = LS.ckinteger(id);
    LS.rawget(tangibles, LuaRegistry, "tangibles");
    LS.rawget(database, tangibles, id);
    if (!LS.istable(database)) {
        luaL_error(L, "Not a tangible ID: %d", nid);
    }
    return LS.result();
}

LuaDefine(tangible_redirect, "tan1,tan2,bulldozetan1", 
    "|Redirect is not working yet") {
    LuaArg actor1, actor2, bldz;
    LuaStack LS(L, actor1, actor2, bldz);
    World *w = World::fetch_global_pointer(L);
    bool bulldoze = LS.ckboolean(bldz);
    Tangible *tan1 = w->tangible_get(LS, actor1);
    if (!tan1->is_an_actor()) {
        luaL_error(L, "redirect source is not an actor");
    }
    if (LS.isnil(actor2)) {
        w->redirects_[tan1->id()] = 0;
    } else {
        Tangible *tan2 = w->tangible_get(LS, actor2);
        tan2->configure_id_pool_for_actor();
        w->redirects_[tan1->id()] = tan2->id();
    }
    if (bulldoze) {
        w->tangible_delete(tan1->id());
    }
    return LS.result();
}

LuaDefine(tangible_id, "tan",
    "|Return the tangible's id number."
    "|This is for debugging only and will be removed"
    "|in the released version.") {
    LuaArg tanobj;
    LuaRet id;
    LuaStack LS(L, tanobj, id);
    int64_t tid = LS.tanid(tanobj);
    if (tid == 0) {
        luaL_error(L, "Not a tangible");
    }
    LS.set(id, tid);
    return LS.result();
}

LuaDefine(tangible_actor, "",
    "|Return the current actor.") {
    LuaRet actor;
    LuaVar tangibles;
    LuaStack LS(L, tangibles, actor);
    World *w = World::fetch_global_pointer(L);
    LS.rawget(tangibles, LuaRegistry, "tangibles");
    LS.rawget(actor, tangibles, w->lthread_actor_id_);
    return LS.result();
}

LuaDefine(tangible_place, "",
    "|Return the current place.") {
    LuaRet place;
    LuaVar tangibles;
    LuaStack LS(L, tangibles, place);
    World *w = World::fetch_global_pointer(L);
    LS.rawget(tangibles, LuaRegistry, "tangibles");
    LS.rawget(place, tangibles, w->lthread_place_id_);
    return LS.result();
}

LuaDefine(tangible_near, "tan,radius,omit_nowhere,omit_self",
    "|Scan near the specified tangible."
    "|If omit_nowhere is true, and the tangible is on the nowhere plane,"
    "|then the scan returns empty.  If omit_self is true, then the "
    "|tangible passed in is omitted from the results.") {
    LuaArg ltan, lradius, lomit_nowhere, lomit_self;
    LuaRet list;
    LuaStack LS(L, ltan, lradius, lomit_nowhere, lomit_self, list);
    World *w = World::fetch_global_pointer(L);
    Tangible *tan = w->tangible_get(LS, ltan);
    double radius = LS.cknumber(lradius);
    bool omit_nowhere = LS.ckboolean(lomit_nowhere);
    bool omit_self = LS.ckboolean(lomit_self);
    const AnimStep &aqback = tan->anim_queue_.back();
    util::IdVector idv = w->plane_map_.scan_radius(aqback.plane(), aqback.xyz().x, aqback.xyz().y, radius, omit_nowhere, tan->id(), omit_self);
    tangible_getall(LS, list, idv);
    return LS.result();
}

LuaDefine(tangible_scan, "plane,x,y,radius,omit_nowhere",
    "|Scan the specified plane."
    "|If omit_nowhere is true, and the plane is 'nowhere', then"
    "|the scan returns empty.") {
    LuaArg lplane, lx, ly, lradius, lomit_nowhere;
    LuaRet list;
    LuaStack LS(L, lplane, lx, ly, lradius, lomit_nowhere, list);
    World *w = World::fetch_global_pointer(L);
    eng::string plane = LS.ckstring(lplane);
    double x = LS.cknumber(lx);
    double y = LS.cknumber(ly);
    double radius = LS.cknumber(lradius);
    bool omit_nowhere = LS.ckboolean(lomit_nowhere);
    util::IdVector idv = w->plane_map_.scan_radius(plane, x, y, radius, omit_nowhere, 0, false);
    tangible_getall(LS, list, idv);
    return LS.result();
}

LuaDefine(wait, "nticks",
    "|Wait the specified number of ticks.") {
    if ((lua_gettop(L) != 1) || (lua_type(L, -1) != LUA_TNUMBER)) {
        luaL_error(L, "Argument to wait must be a number.");
    }
    return lua_yield(L, 1);
}

LuaDefine(tangible_nopredict, "",
    "|Stop predictive execution of this thread.") {
    if (lua_gettop(L) != 0) {
        luaL_error(L, "tangible.nopredict takes no arguments");
    }
    World *w = World::fetch_global_pointer(L);
    if (util::world_type_authoritative(w->world_type_)) {
        return 0;
    } else {
        return lua_yield(L, 0);
    }
}

LuaDefine(pprint, "obj1,obj2,...",
    "|Pretty-print object or objects.") {
    World *w = World::fetch_global_pointer(L);
    std::ostream *ostream = w->lthread_print_stream();
    LuaStack LS(L);
    for (int i = 1; i <= lua_gettop(L); i++) {
        LuaSpecial root(i);
        pprint(LS, root, true, ostream);
        (*ostream) << std::endl;
    }
    return LS.result();
}

LuaDefine(print, "obj1,obj2,...", 
    "|Print object or objects.") {
    World *w = World::fetch_global_pointer(L);
    std::ostream *ostream = w->lthread_print_stream();
    LuaStack LS(L);
    int n = lua_gettop(L);
    for (int i = 1; i <= n; i++) {
        LuaSpecial root(i);
        atomic_print(LS, root, false, ostream);
        if (i < n) (*ostream) << " ";
    }
    (*ostream) << std::endl;
    return LS.result();
}

LuaDefine(doc, "function",
    "|Print documentation for specified function.") {
    World *w = World::fetch_global_pointer(L);
    std::ostream *ostream = w->lthread_print_stream();
    LuaArg func;
    LuaStack LS(L, func);
    eng::string doc = SourceDB::function_docs(LS, func);
    if (doc == "") {
        (*ostream) << "no doc found" << std::endl;
    }
    (*ostream) << doc;
    return LS.result();
}