#include "traceback.hpp"

#include <cstring>
#include <cassert>

#define TRACEBACK_LEVELS1 12
#define TRACEBACK_LEVELS2 10

// Call this with the error message on top of the stack.
// The error message is replaced with a traceback.
//
int traceback_coroutine(lua_State *L) {
    lua_checkstack(L, 20);
    int top = lua_gettop(L);

    // Convert message to a string
    if (!lua_tostring(L, top)) {
        luaL_callmeta(L, top, "__tostring");
        // If callmeta didn't produce exactly one string, clear the stack
        // and push "unknown error"
        if ((lua_gettop(L) == top + 1) && (lua_tostring(L, -1))) {
            lua_remove(L, top);
        } else {
            lua_settop(L, top - 1);
            lua_pushstring(L, "unknown error");
        }
    }

    // Append the traceback.
    lua_Debug ar;
    int firstpart = 1;
    bool any = false;
    for (int level = 0; lua_getstack(L, level, &ar); level++) {
        if (level > TRACEBACK_LEVELS1 && firstpart) {
            /* no more than `LEVELS2' more levels? */
            if (!lua_getstack(L, level + TRACEBACK_LEVELS2, &ar))
                level--; /* keep going */
            else {
                lua_pushliteral(L, "\n\t...");                 /* too many levels */
                while (lua_getstack(L, level + TRACEBACK_LEVELS2, &ar)) /* find last levels */
                    level++;
            }
            firstpart = 0;
            continue;
        }
        
        lua_getinfo(L, "Snlf", &ar);

        eng::string storedname;
        {
            lua_getfield(L, LUA_REGISTRYINDEX, "funcnames");
            lua_pushvalue(L, -2);
            lua_rawget(L, -2);
            const char *fname = lua_tostring(L, -1);
            if (fname) storedname = fname;
            lua_pop(L, 3);
        }

        if ((!any) && (*ar.what == 'C') && (ar.name != 0)) {
            if (strcmp(ar.name, "__newindex") == 0) continue;
        }
        if ((ar.currentline > 0) || (*ar.namewhat != 0) || (*ar.what != 'C')) {
            any = true;
            lua_pushliteral(L, "\n\t");

            if (strcmp(ar.short_src, "<console>")==0) {
                lua_pushstring(L, "in the console in");
            } else if (strcmp(ar.short_src, "[C]")==0) {
                lua_pushstring(L, "in builtin C++ ");
            } else if (ar.currentline > 0) {
                lua_pushfstring(L, "in %s line %d in", ar.short_src, ar.currentline);
            } else {
                lua_pushfstring(L, "in %s in", ar.short_src);
            }

            if (!storedname.empty()) {
                lua_pushfstring(L, " function " LUA_QS, storedname.c_str());
            } else if (*ar.namewhat != 0) {
                lua_pushfstring(L, " function " LUA_QS, ar.name);
            } else if (*ar.what == 'm') {
                lua_pushfstring(L, " top-level expression ");
            } else {
                lua_pushliteral(L, " unknown function");
            }

            if (1 + lua_gettop(L) - top > 5) {
                lua_concat(L, 1 + lua_gettop(L) - top);
            }
        }
    }
    lua_pushstring(L, "\n");
    if (1 + lua_gettop(L) - top > 1) {
        lua_concat(L, 1 + lua_gettop(L) - top);
    }
    return 1;
}


eng::string traceback_pcall(lua_State *L, int narg, int nret) {
    int status;
    int base = lua_gettop(L) - narg; /* function index */
    lua_pushcfunction(L, traceback_coroutine); /* push traceback function */
    lua_insert(L, base);             /* put it under chunk and args */
    status = lua_pcall(L, narg, nret, base);
    lua_remove(L, base); /* remove traceback function */
    if (status != LUA_OK) {
        const char *msg = lua_tostring(L, -1);
        if ((msg == NULL) || (msg[0] == 0)) {
            msg = "unknown error";
        }
        eng::string result = msg;
        assert(result != "attempt to yield from outside a coroutine");
        return result;
    }
    return "";
}
