#include "wrap-string.hpp"

#include "keywords.hpp"
#include "util.hpp"

static LuaToken token_error("error");
static LuaToken token_found("found");

LuaKeywordParser::LuaKeywordParser(const LuaCoreStack &LS0, LuaSlot slot)
    : keytab(slot.index()), LS(LS0.state()) {
    lua_State *L = LS0.state();
    lua_pushnil(L); found.index_ = lua_gettop(L);
    lua_pushnil(L); error.index_ = lua_gettop(L);
    lua_pushnil(L); key.index_ = lua_gettop(L);
    lua_pushnil(L); val.index_ = lua_gettop(L);

    istable = LS.istable(keytab);
    if (istable) {
        LS.rawget(found, keytab, token_found);
        if (!LS.istable(found)) {
            LS.set(found, LuaNewTable);
            LS.rawset(keytab, token_found, found);
        }
    }
}

bool LuaKeywordParser::optional(LuaSlot out, std::string_view kw) {
    if (!istable) {
        LS.set(out, LuaNil);
        return false;
    }

    LS.rawget(out, keytab, kw);
    if (!LS.isnil(out)) {
        LS.rawset(found, kw, true);
        return true;
    } else {
        return false;
    }
};

bool LuaKeywordParser::required(LuaSlot out, std::string_view kw) {
    if (!istable) {
        LS.set(out, LuaNil);
        return false;
    }

    LS.rawget(out, keytab, kw);
    if (!LS.isnil(out)) {
        LS.rawset(found, kw, true);
        return true;
    } else {
        LS.rawget(error, keytab, token_error);
        if (!LS.isstring(error)) {
            LS.rawset(keytab, token_error, util::ss("required keyword argument not present: ", kw));
        }
        return false;
    }
};

eng::string LuaKeywordParser::check() {
    if (!istable) {
        return "keyword arguments must be a table";
    }

    LS.rawget(error, keytab, token_error);
    auto str = LS.trystring(error);
    if (str.has_value()) return str.value();

    return "";
}

eng::string LuaKeywordParser::final_check() {
    if (!istable) {
        return "keyword arguments must be a table";
    }

    LS.rawget(error, keytab, token_error);
    auto str = LS.trystring(error);
    if (str.has_value()) return str.value();

    LS.set(key, LuaNil);
    while (LS.next(keytab, key, val)) {
        if (LS.istoken(key)) {
            continue;
        }
        auto kw = LS.trystringview(key);
        if (!kw.has_value()) {
            return "keyword arguments include a non-string key";
        }
        LS.rawget(val, found, key);
        if (!LS.rawequal(val, true)) {
            return util::ss("unrecognized keyword argument: ", kw.value());
        }
    }
    return "";
}

void LuaKeywordParser::check_throw() {
    eng::string err = check();
    if (!err.empty()) {
        luaL_error(LS.state(), "%s", err.c_str());
    }
}

void LuaKeywordParser::final_check_throw() {
    eng::string err = final_check();
    if (!err.empty()) {
        luaL_error(LS.state(), "%s", err.c_str());
    }
}

static const char *kwdoc =
    "|Parse lua keyword arguments."
    "|"
    "|The keywords module is used to help parse keyword arguments"
    "|for functions.  The error handling is strong, it throws useful"
    "|error messages when the caller passes in incorrect arguments."
    "|This is a typical example of how it is used:"
    "|"
    "|   function drawrect(args)"
    "|      local x1,y1 = keywords.required(args, 'x1', 'y1')"
    "|      local x2,y2 = keywords.required(args, 'x2', 'y2')"
    "|      local color = keywords.optional(args, 'color')"
    "|      keywords.finalcheckthrow(args)"
    "|      ..."
    "|   end"
    "|"
    "|The function above expects four required keyword arguments and one"
    "|optional one.  The example code above handles all the following"
    "|error cases:"
    "|"
    "|  * That 'args' actually is a table."
    "|  * That the required keywords x1,y1,x2,y2 are all present."
    "|  * That the table doesn't contain extraneous unknown keywords."
    "|"
    "|The functions keywords.required and keywords.optional don't throw"
    "|lua errors.  Instead, they store status information in the keyword"
    "|table itself.  Using the status information, the function"
    "|keywords.finalcheckthrow will throw an error if anything went wrong"
    "|along the way."
    "|"
    "|You can check errors when you're not finished parsing using the"
    "|function keywords.checkthrow() instead of keywords.finalcheckthrow()."
    "|Unlike finalcheckthrow, this function doesn't verify the absence of"
    "|extraneous keywords, which can only be done at the end."
    "|"
    "|If you don't want to throw errors at all, you can use"
    "|keywords.finalcheck() instead of keywords.finalcheckthrow()."
    "|This returns the error message, rather than throwing it."
    "|";


LuaDefine(keywords_optional, "table, keyword, keyword, keyword...", kwdoc) {
    LuaArg table;
    LuaExtraArgs keywords;
    LuaDefStack LS(L, table, keywords);
    LuaKeywordParser KP(LS, table);

    if (keywords.size() < 1) {
        luaL_error(L, "expected at least one keyword");
        return 0;
    }
    for (int i = 0; i < keywords.size(); i++) {
        eng::string kw = LS.ckstring(keywords[i], "keyword");
        KP.optional(keywords[i], kw);
    }

    // Return the results without using LS.result, because it doesn't
    // support multiple return values.
    lua_settop(L, keywords[keywords.size() - 1].index());
    return keywords.size();
}

LuaDefine(keywords_required, "table, keyword, keyword, keyword...", kwdoc) {
    LuaArg table;
    LuaExtraArgs keywords;
    LuaDefStack LS(L, table, keywords);
    LuaKeywordParser KP(LS, table);

    if (keywords.size() < 1) {
        luaL_error(L, "expected at least one keyword");
        return 0;
    }
    for (int i = 0; i < keywords.size(); i++) {
        eng::string kw = LS.ckstring(keywords[i], "keyword");
        KP.required(keywords[i], kw);
    }

    // Return the results without using LS.result, because it doesn't
    // support multiple return values.
    lua_settop(L, keywords[keywords.size() - 1].index());
    return keywords.size();
}

LuaDefine(keywords_check, "table", kwdoc) {
    LuaArg table;
    LuaRet result;
    LuaDefStack LS(L, table, result);
    LuaKeywordParser KP(LS, table);
    eng::string err = KP.check();
    if (!err.empty()) {
        LS.set(result, err);
    }
    return LS.result();
}

LuaDefine(keywords_finalcheck, "table", kwdoc) {
    LuaArg table;
    LuaRet result;
    LuaDefStack LS(L, table, result);
    LuaKeywordParser KP(LS, table);
    eng::string err = KP.final_check();
    if (!err.empty()) {
        LS.set(result, err);
    }
    return LS.result();
}

LuaDefine(keywords_checkthrow, "table", kwdoc) {
    LuaArg table;
    LuaDefStack LS(L, table);
    LuaKeywordParser KP(LS, table);
    KP.check_throw();
    return LS.result();
}

LuaDefine(keywords_finalcheckthrow, "table", kwdoc) {
    LuaArg table;
    LuaDefStack LS(L, table);
    LuaKeywordParser KP(LS, table);
    KP.final_check_throw();
    return LS.result();
}
