int Lua_print(lua_State *L) {
    Scratch scratch;
    int nargs = lua_gettop(L);
    View *null_view = GetView(NullViewID);
    for (int i = 1; i <= nargs; i += 1) {
        String string = lua_tostring(L, i);
        Command_Appendf(null_view, "%S ", string);
    }
    Command_Appendf(null_view, "\n");
    lua_pop(L, nargs);
    return 0;
}

int Lua_Print(lua_State *L) {
    Scratch scratch;
    int nargs = lua_gettop(L);
    for (int i = 1; i <= nargs; i += 1) {
        String string = lua_tostring(L, i);
        Command_Appendf(TraceView, "%S ", string);
    }
    Command_Appendf(TraceView, "\n");
    lua_pop(L, nargs);
    return 0;
}

int Lua_Kill(lua_State *L) {
    BSet main = GetLastActiveLayoutSet();
    KillProcess(main.view);
    return 0;
}

int Lua_GetLoadWord(lua_State *L) {
    BSet  active = GetActiveSet();
    Range range  = active.view->carets[0].range;
    if (GetSize(range) == 0) {
        range = EncloseLoadWord(active.buffer, range.min);
    }

    Scratch scratch;
    String  string = AllocCharString(scratch, active.buffer, range);
    lua_pushlstring(L, string.data, string.len);
    return 1;
}

int Lua_BufferExists(lua_State *L) {
    String string = lua_tostring(L, 1);
    lua_pop(L, 1);
    Buffer *buffer = GetBuffer(string);
    lua_pushboolean(L, buffer != NULL);
    return 1;
}

int Lua_GetSelection(lua_State *L) {
    Scratch scratch;
    BSet main = GetLastActiveLayoutSet();
    String16 string16 = GetString(main.buffer, main.view->carets[0].range);
    String string = ToString(scratch, string16);
    lua_pushlstring(L, string.data, string.len);
    return 1;
}

int Lua_GetEntireBuffer(lua_State *L) {
    Scratch scratch;
    BSet main = GetLastActiveLayoutSet();
    String16 string16 = GetString(main.buffer);
    String string = ToString(scratch, string16);
    lua_pushlstring(L, string.data, string.len);
    return 1;
}

int Lua_GetClipboard(lua_State *L) {
    Scratch scratch;
    String string = ToString(scratch, SavedClipboardString);
    lua_pushlstring(L, string.data, string.len);
    return 1;
}

int Lua_GetFilename(lua_State *L) {
    BSet main = GetLastActiveLayoutSet();
    lua_pushlstring(L, main.buffer->name.data, main.buffer->name.len);
    return 1;
}

int Lua_GetLine(lua_State *L) {
    BSet main = GetLastActiveLayoutSet();
    Caret caret = main.view->carets[0];
    Int front = GetFront(caret);
    Int line = PosToLine(main.buffer, front);
    lua_pushinteger(L, line + 1);
    return 1;
}

int Lua_FileExists(lua_State *L) {
    String path = luaL_checkstring(L, 1);
    lua_pop(L, 1);
    bool exists = FileExists(path);
    lua_pushboolean(L, exists);
    return 1;
}

int Lua_GetWorkDir(lua_State *L) {
    lua_pushlstring(L, WorkDir.data, WorkDir.len);
    return 1;
}

int Lua_GetExeDir(lua_State *L) {
    Scratch scratch;
    String exe_dir = GetExeDir(scratch);
    lua_pushlstring(L, exe_dir.data, exe_dir.len);
    return 1;
}

int Lua_GetMainDir(lua_State *L) {
    String name = Command_GetMainDir();
    lua_pushlstring(L, name.data, name.len);
    return 1;
}

int Lua_KillWindow(lua_State *L) {
    BSet set = GetLastActiveLayoutSet();
    set.window->kill = true;
    return 0;
}

static void HookLuaForceExit(lua_State *L, lua_Debug *debug) {
    SDL_PumpEvents();
    int            numkeys = 0;
    const bool *keys    = SDL_GetKeyboardState(&numkeys);
    if (keys[SDL_SCANCODE_F9])
        luaL_error(L, "lua execution got interrupted");
}

double GetStyleFloat(String name, double default_float) {
    double result = default_float;
    lua_getglobal(LuaState, "Style");
    defer { lua_pop(LuaState, 1); };
    if (lua_istable(LuaState, -1)) {
        lua_pushlstring(LuaState, name.data, name.len);
        lua_gettable(LuaState, -2);
        defer { lua_pop(LuaState, 1); };
        if (lua_isnumber(LuaState, -1) || lua_isboolean(LuaState, -1) || lua_isinteger(LuaState, -1)) {
            lua_Number num = lua_tonumber(LuaState, -1);
            result = (double)num;
        }
    }
    return result;
}

Int GetStyleInt(String name, Int default_int) {
    Int result = default_int;
    lua_getglobal(LuaState, "Style");
    defer { lua_pop(LuaState, 1); };
    if (lua_istable(LuaState, -1)) {
        lua_pushlstring(LuaState, name.data, name.len);
        lua_gettable(LuaState, -2);
        defer { lua_pop(LuaState, 1); };
        if (lua_isnumber(LuaState, -1) || lua_isboolean(LuaState, -1) || lua_isinteger(LuaState, -1)) {
            lua_Integer num = lua_tointeger(LuaState, -1);
            result          = (Int)num;
        }
    }
    return result;
}

String GetStyleString(String name, String default_string) {
    String result = default_string;
    lua_getglobal(LuaState, "Style");
    defer { lua_pop(LuaState, 1); };
    if (lua_istable(LuaState, -1)) {
        lua_pushlstring(LuaState, name.data, name.len);
        lua_gettable(LuaState, -2);
        defer { lua_pop(LuaState, 1); };
        if (lua_isstring(LuaState, -1)) {
            const char *string = lua_tostring(LuaState, -1);
            result             = Intern(&GlobalInternTable, string);
        }
    }
    return result;
}

Color GetColor(String name, Color default_color) {
    Color result = default_color;
    lua_getglobal(LuaState, "Color");
    defer { lua_pop(LuaState, 1); };
    if (lua_istable(LuaState, -1)) {
        lua_pushlstring(LuaState, name.data, name.len);
        lua_gettable(LuaState, -2);
        defer { lua_pop(LuaState, 1); };
        if (lua_isnumber(LuaState, -1)) {
            lua_Integer num = lua_tointeger(LuaState, -1);
            result.r        = (uint8_t)((0xFF000000 & num) >> 24);
            result.g        = (uint8_t)((0x00FF0000 & num) >> 16);
            result.b        = (uint8_t)((0x0000FF00 & num) >> 8);
            result.a        = (uint8_t)((0x000000FF & num) >> 0);
        }
    }
    return result;
}

String GetFieldString(lua_State *L, String name) {
    String result = {};
    if (lua_istable(L, -1)) {
        lua_pushlstring(L, name.data, name.len);
        lua_gettable(L, -2);
        defer { lua_pop(L, 1); };
        if (lua_isstring(L, -1)) {
            result = lua_tostring(L, -1);
        }
    }
    return result;
}

Int GetFieldInt(lua_State *L, const char *name) {
    lua_getfield(L, -1, name);
    lua_Integer num = lua_tointeger(L, -1);
    lua_pop(L, 1);
    return (Int)num;
}

double GetFieldFloat(lua_State *L, const char *name) {
    lua_getfield(L, -1, name);
    double num = lua_tonumber(L, -1);
    lua_pop(L, 1);
    return num;
}

const char *GetFieldString(lua_State *L, const char *name) {
    lua_getfield(L, -1, name);
    const char *result = lua_tostring(L, -1);

    lua_pop(L, 1);
    return result;
}

void PushEvent(lua_State *L, Event *event) {
    lua_createtable(L, 0, EVENT_FIELD_COUNT);
#define lua_pushInt lua_pushinteger
#define lua_pushString lua_pushstring
#define lua_pushFloat lua_pushnumber
#define X(TYPE, KIND, NAME)         \
    lua_push##KIND(L, event->NAME); \
    lua_setfield(L, -2, #NAME);
    EVENT_FIELDS
#undef X
}

// :Event
int Lua_Play(lua_State *L) {
    if (!lua_istable(L, -1)) luaL_error(L, "expected a table of events");
    defer { lua_pop(L, 1); };

    int size = (int)lua_rawlen(L, -1);
    for (int i = 0; i < size; i += 1) {
        lua_geti(L, -1, i + 1);
        if (!lua_istable(L, -1)) luaL_error(L, "expected a table of events");
        defer { lua_pop(L, 1); };

        Event event = {};
#define X(TYPE, KIND, NAME) event.NAME = (TYPE)GetField##KIND(L, #NAME);
        EVENT_FIELDS
#undef X
        Add(&EventPlayback, event);
    }

    return 0;
}

void          ReloadStyle();
extern String BaseLuaConfig;

void LoadLuaBuffer(Buffer *lua_buffer) {
    if (!lua_buffer) return;
    ReportConsolef("reloading config: %S", lua_buffer->name);

    Scratch scratch;
    String  string = AllocCharString(scratch, lua_buffer);
    if (luaL_dostring(LuaState, string.data) == LUA_OK) {
        if (lua_isstring(LuaState, -1)) {
            const char *text = lua_tostring(LuaState, -1);
            ReportConsolef(text);
            lua_pop(LuaState, 1);
        }
    } else {
        const char *error_message = lua_tostring(LuaState, -1);
        ReportWarningf("Failed to load user config! %s", error_message);
        lua_pop(LuaState, 1);
    }
    lua_buffer->user_change_id = lua_buffer->change_id;
}

void ReloadLuaConfigs(bool reload = false) {
    if (LuaConfigBuffer && !LuaConfigBuffer->dirty && LuaConfigBuffer->change_id != LuaConfigBuffer->user_change_id) {
        reload = true;
    }
    if (LuaProjectBuffer && !LuaProjectBuffer->dirty && LuaProjectBuffer->change_id != LuaProjectBuffer->user_change_id) {
        reload = true;
    }
    if (reload == false) {
        return;
    }

    LoadLuaBuffer(LuaConfigBuffer);
    LoadLuaBuffer(LuaProjectBuffer);

    ReloadStyle();
    ReloadFont();
    For(Windows) {
        it->draw_scrollbar    = StyleDrawScrollbar;
        it->draw_line_numbers = StyleDrawLineNumbers;
    }
}

bool CallLuaFunc(char *func_name, int arg_count, int ret_count) {
    if (lua_pcall(LuaState, arg_count, ret_count, 0) != 0) {
        const char *error_message = lua_tostring(LuaState, -1);
        ReportWarningf("Failed to call a lua function: %s! %s", func_name, error_message);
        lua_pop(LuaState, 1);
        return false;
    }
    return true;
}

struct OnOpenResult {
    String kind;
    String file_path;
    Int line, col;
    String working_dir;
    String cmd;
};

OnOpenResult CallOnOpen(Allocator allocator, String path, String meta) {
    lua_getglobal(LuaState, "OnOpen");
    lua_pushlstring(LuaState, path.data, path.len);
    lua_pushlstring(LuaState, meta.data, meta.len);
    if (!CallLuaFunc("OnOpen", 2, 1)) {
        return {};
    }

    String file_path   = GetFieldString(LuaState, "file_path");
    String line_string = GetFieldString(LuaState, "line");
    String col_string  = GetFieldString(LuaState, "col");
    String cmd         = GetFieldString(LuaState, "cmd");
    String working_dir = GetFieldString(LuaState, "working_dir");
    String kind        = GetFieldString(LuaState, "kind");

    OnOpenResult result = {};
    result.cmd = cmd;
    result.working_dir = working_dir;
    result.file_path = file_path;
    if (!IsAbsolute(result.file_path)) {
        String dir = Command_GetMainDir();
        result.file_path = Format(allocator, "%S/%S", dir, result.file_path);
    }
    if (col_string.len) {
        result.col = strtoll(col_string.data, NULL, 10);
    } else {
        result.col = -1;
    }
    if (line_string.len) {
        result.line = strtoll(line_string.data, NULL, 10);
    } else {
        result.line = -1;
    }
    result.kind = kind;


    return result;
}

bool CallIsCode(String path, String meta = "") {
    lua_getglobal(LuaState, "IsCode");
    lua_pushlstring(LuaState, path.data, path.len);
    lua_pushlstring(LuaState, meta.data, meta.len);
    if (!CallLuaFunc("IsCode", 2, 1)) {
        return false;
    }

    bool result = lua_toboolean(LuaState, -1);
    lua_pop(LuaState, 1);
    return result;
}

void CallOnSave(BufferID buffer_id) {
    lua_getglobal(LuaState, "OnSave");
    lua_pushinteger(LuaState, buffer_id.id);
    CallLuaFunc("OnSave", 1, 0);
}

bool CallOnCommand(Event *event) {
    lua_getglobal(LuaState, "OnCommand");
    PushEvent(LuaState, event);
    CallLuaFunc("OnCommand", 1, 1);
    bool result = lua_toboolean(LuaState, -1);
    lua_pop(LuaState, 1);
    return result;
}

void CallLuaOnUpdate(Event *event) {
    lua_getglobal(LuaState, "OnUpdate");
    PushEvent(LuaState, event);
    CallLuaFunc("OnUpdate", 1, 0);
}

void CallLuaOnInit() {
    lua_getglobal(LuaState, "OnInit");
    CallLuaFunc("OnInit", 0, 0);
}

void InitLuaConfig() {
    LuaState = luaL_newstate();
    luaL_openlibs(LuaState);
    lua_sethook(LuaState, HookLuaForceExit, LUA_MASKCOUNT, 100000000);

    for (int i = 0; LuaFunctions[i].name; i += 1) {
        lua_pushcfunction(LuaState, LuaFunctions[i].func);
        lua_setglobal(LuaState, LuaFunctions[i].name);
    }

#if OS_WINDOWS
    lua_pushinteger(LuaState, 0);
#else
    lua_pushinteger(LuaState, 1);
#endif
    lua_setglobal(LuaState, "OS_VALUE");

    // Init base config, test that it works and initialize the lua stuff
    if (!luaL_dostring(LuaState, BaseLuaConfig.data) == LUA_OK) {
        const char *error_message = lua_tostring(LuaState, -1);
        ReportErrorf("Failed to load base lua config! %s", error_message);
        lua_pop(LuaState, 1);
        Assert(!"Invalid codepath");
    }

    // Init user config
    Buffer *lua_buffer = NULL;

    Scratch scratch;
    String lua_config_exe = Format(scratch, "%S/init.lua", GetExeDir(scratch));
    if (FileExists(lua_config_exe)) {
        lua_buffer = BufferOpenFile(lua_config_exe);
    }

    if (lua_buffer == NULL) {
        String lua_config_remote = Format(scratch, "%S/init.lua", ConfigDir);
// #if DEBUG_BUILD
//         // WARNING! Delete config to make sure we are running this code more frequently
//         SDL_RemovePath(lua_config_remote.data);
//         ReportConsolef("deleting config for debug purposes!");
// #endif
        lua_buffer = BufferOpenFile(lua_config_remote);
        if (lua_buffer->len == 0) {
            String16 string16 = ToString16(scratch, BaseLuaConfig);
            RawReplaceText(lua_buffer, {}, string16);
            ReportConsolef("no config at: %S - creating config buffer", lua_config_remote);
        }
    }




    LuaConfigBuffer = lua_buffer;
    ReloadLuaConfigs(true);
    CallLuaOnInit();
}
