#include "lua/wherigo.h"
#include "app.h"
#include "lua/game_engine.h"
#include "lua/geo_utils.h"
#include "lua/media_manager.h"
#include "lua/zobject.h"
#include "lua/ztimer.h"
#include "ui/wherigo_dialog.h"

extern "C" {
#include <lauxlib.h>
#include <lua.h>
}

#include <wx/choicdlg.h>
#include <wx/file.h>
#include <wx/filename.h>
#include <wx/log.h>
#include <wx/process.h>
#include <wx/textdlg.h>
#include <wx/utils.h>

#include <algorithm>
#include <cmath>
#include <string>
#include <vector>

namespace wherigo {

// ── Distance / Bearing metatables ─────────────────────────────────────────────

// __call on Distance: distance("m") → number
static int distance_call(lua_State *L) {
    luaL_checktype(L, 1, LUA_TTABLE);
    const char *units = luaL_optstring(L, 2, "meters");
    lua_getfield(L, 1, "value");
    const double meters = lua_tonumber(L, -1);
    lua_pop(L, 1);
    lua_pushnumber(L, metersToUnit(meters, units));
    return 1;
}

// GetValue closure: captured Distance table as upvalue 1
static int distance_getvalue_upval(lua_State *L) {
    const char *units = luaL_optstring(L, 1, "meters");
    lua_pushvalue(L, lua_upvalueindex(1));   // Distance table
    lua_getfield(L, -1, "value");
    const double meters = lua_tonumber(L, -1);
    lua_pop(L, 2);
    lua_pushnumber(L, metersToUnit(meters, units));
    return 1;
}

// __call on Bearing: bearing() → number (degrees)
static int bearing_call(lua_State *L) {
    luaL_checktype(L, 1, LUA_TTABLE);
    lua_getfield(L, 1, "value");
    const double val = lua_tonumber(L, -1);
    lua_pop(L, 1);
    lua_pushnumber(L, std::fmod(val + 360.0, 360.0));
    return 1;
}

static void setupMetatables(lua_State *L) {
    luaL_newmetatable(L, "Wherigo.Distance");
    lua_pushcfunction(L, distance_call);
    lua_setfield(L, -2, "__call");
    lua_pop(L, 1);

    luaL_newmetatable(L, "Wherigo.Bearing");
    lua_pushcfunction(L, bearing_call);
    lua_setfield(L, -2, "__call");
    lua_pop(L, 1);
}

// ── App-level helpers ─────────────────────────────────────────────────────────

// Returns the loaded cartridge's name, or "Wherigo" as fallback.
static wxString cartridgeTitle() {
    if (auto *app = dynamic_cast<cApp*>(wxTheApp);
        app && app->isCartridgeLoaded())
        return wxString::FromUTF8(app->getCartridge()->cartridgeName());
    return "Wherigo";
}

// ── Completion code helper ────────────────────────────────────────────────────

std::string getCompletionCode(lua_State *L) {
    lua_getglobal(L, "Player");
    if (!lua_istable(L, -1)) { lua_pop(L, 1); return ""; }
    lua_getfield(L, -1, "CompletionCode");
    std::string code;
    if (lua_isstring(L, -1)) {
        code = lua_tostring(L, -1);
        if (code.length() > 15) code = code.substr(0, 15);
    }
    lua_pop(L, 2);
    return code;
}

// ── Wherigo API functions ─────────────────────────────────────────────────────

static int wherigo_RequestSync(lua_State *L) {
    auto completionCode = getCompletionCode(L);
    if (completionCode.empty()) {
        if (auto *app = dynamic_cast<cApp*>(wxTheApp);
            app && app->isCartridgeLoaded())
            completionCode = app->getCartridge()->completionCode().substr(0, 15);
    }
    const wxString msg = wxString::Format(
        "Completion Code:\n\n%s\n\nEnter this code on wherigo.com to verify your completion.",
        completionCode.c_str());
    wherigo::WherigoMessageDialog dlg(nullptr, msg, cartridgeTitle() + " – Abschluss", {"OK"});
    dlg.ShowModal();
    return 0;
}

static int wherigo_ZonePoint(lua_State *L) {
    const lua_Number lat = luaL_checknumber(L, 1);
    const lua_Number lng = luaL_checknumber(L, 2);
    const lua_Number alt = luaL_optnumber(L, 3, 0.0);
    lua_newtable(L);
    lua_pushnumber(L, lat); lua_setfield(L, -2, "latitude");
    lua_pushnumber(L, lng); lua_setfield(L, -2, "longitude");
    lua_pushnumber(L, alt); lua_setfield(L, -2, "altitude");
    lua_pushstring(L, "ZonePoint"); lua_setfield(L, -2, "_classname");
    return 1;
}

static int wherigo_Distance(lua_State *L) {
    const lua_Number value = luaL_checknumber(L, 1);
    const char *unit = luaL_optstring(L, 2, "meters");
    const double meters = distanceToMeters(value, unit);

    lua_newtable(L);
    lua_pushnumber(L, meters);  lua_setfield(L, -2, "value");
    lua_pushstring(L, "Distance"); lua_setfield(L, -2, "_classname");

    // GetValue closure: captures this Distance table as upvalue
    lua_pushvalue(L, -1);
    lua_pushcclosure(L, distance_getvalue_upval, 1);
    lua_setfield(L, -2, "GetValue");

    luaL_getmetatable(L, "Wherigo.Distance");
    lua_setmetatable(L, -2);
    return 1;
}

static int wherigo_Bearing(lua_State *L) {
    const double deg = luaL_optnumber(L, 1, 0.0);
    pushBearing(L, deg);
    return 1;
}

// If arg1 is a plain properties table (no _classname), use it as the object base
// (mirrors the reference: "if type(cartridge)=='table' and not cartridge._classname then self=cartridge").
// Otherwise create a fresh table.
static void pushObjectBase(lua_State *L) {
    if (lua_istable(L, 1)) {
        lua_getfield(L, 1, "ClassName");        // our objects use "ClassName" (not "_classname")
        const bool isPlainProps = lua_isnil(L, -1);
        lua_pop(L, 1);
        if (isPlainProps) { lua_pushvalue(L, 1); return; }
    }
    lua_newtable(L);
}

// Set field to value only if currently nil/unset — preserves props from caller's table
#define SET_DEFAULT_BOOL(key, val) do { \
    lua_getfield(L, -1, key); \
    if (lua_isnil(L, -1)) { lua_pop(L, 1); lua_pushboolean(L, val); lua_setfield(L, -2, key); } \
    else { lua_pop(L, 1); } } while(0)

#define SET_DEFAULT_STR(key, val) do { \
    lua_getfield(L, -1, key); \
    if (lua_isnil(L, -1)) { lua_pop(L, 1); lua_pushstring(L, val); lua_setfield(L, -2, key); } \
    else { lua_pop(L, 1); } } while(0)

#define SET_DEFAULT_NUM(key, val) do { \
    lua_getfield(L, -1, key); \
    if (lua_isnil(L, -1)) { lua_pop(L, 1); lua_pushnumber(L, val); lua_setfield(L, -2, key); } \
    else { lua_pop(L, 1); } } while(0)

#define SET_DEFAULT_TABLE(key) do { \
    lua_getfield(L, -1, key); \
    if (lua_isnil(L, -1)) { lua_pop(L, 1); lua_newtable(L); lua_setfield(L, -2, key); } \
    else { lua_pop(L, 1); } } while(0)

static int wherigo_Zone(lua_State *L) {
    pushObjectBase(L);
    lua_pushstring(L, "Zone");         lua_setfield(L, -2, "ClassName");
    SET_DEFAULT_BOOL("Active",   0);
    SET_DEFAULT_BOOL("Visible",  1);
    SET_DEFAULT_TABLE("Points");
    // State machine fields – always reset to initial state
    lua_pushstring(L, "NotInRange");   lua_setfield(L, -2, "State");
    lua_pushstring(L, "NotInRange");   lua_setfield(L, -2, "_state");
    lua_pushboolean(L, 0);             lua_setfield(L, -2, "_inside");
    SET_DEFAULT_STR("ShowObjects", "OnEnter");
    lua_getfield(L, -1, "ProximityRange");
    if (lua_isnil(L, -1)) { lua_pop(L, 1); pushDistance(L, 60.0); lua_setfield(L, -2, "ProximityRange"); }
    else { lua_pop(L, 1); }
    lua_getfield(L, -1, "DistanceRange");
    if (lua_isnil(L, -1)) { lua_pop(L, 1); pushDistance(L, -0.3048); lua_setfield(L, -2, "DistanceRange"); }
    else { lua_pop(L, 1); }
    pushDistance(L, 0.0);              lua_setfield(L, -2, "CurrentDistance");
    pushBearing(L, 0.0);               lua_setfield(L, -2, "CurrentBearing");
    lua_pushcfunction(L, zobject_Contains); lua_setfield(L, -2, "Contains");
    lua_pushcfunction(L, zobject_MoveTo);   lua_setfield(L, -2, "MoveTo");
    return 1;
}

static int wherigo_ZCharacter(lua_State *L) {
    pushObjectBase(L);
    lua_pushstring(L, "ZCharacter");        lua_setfield(L, -2, "ClassName");
    SET_DEFAULT_BOOL("Active",  0);
    SET_DEFAULT_BOOL("Visible", 1);
    lua_pushcfunction(L, zobject_MoveTo);    lua_setfield(L, -2, "MoveTo");
    lua_pushcfunction(L, zobject_Contains);  lua_setfield(L, -2, "Contains");
    return 1;
}

static int wherigo_ZItem(lua_State *L) {
    pushObjectBase(L);
    lua_pushstring(L, "ZItem");             lua_setfield(L, -2, "ClassName");
    SET_DEFAULT_BOOL("Active",  0);
    SET_DEFAULT_BOOL("Visible", 1);
    lua_pushcfunction(L, zobject_MoveTo);    lua_setfield(L, -2, "MoveTo");
    lua_pushcfunction(L, zobject_Contains);  lua_setfield(L, -2, "Contains");
    return 1;
}

static int wherigo_ZTask(lua_State *L) {
    pushObjectBase(L);
    lua_pushstring(L, "ZTask");   lua_setfield(L, -2, "ClassName");
    SET_DEFAULT_BOOL("Active",    0);
    SET_DEFAULT_BOOL("Complete",  0);
    return 1;
}

static int wherigo_ZTimer(lua_State *L) {
    pushObjectBase(L);
    lua_pushstring(L, "ZTimer");       lua_setfield(L, -2, "ClassName");
    SET_DEFAULT_STR("Type", "Countdown");
    SET_DEFAULT_NUM("Duration", 0);
    lua_pushboolean(L, 0);             lua_setfield(L, -2, "Running");
    lua_pushcfunction(L, ztimer_Start); lua_setfield(L, -2, "Start");
    lua_pushcfunction(L, ztimer_Stop);  lua_setfield(L, -2, "Stop");
    lua_pushcfunction(L, ztimer_Reset); lua_setfield(L, -2, "Reset");
    return 1;
}

static int wherigo_ZInput(lua_State *L) {
    pushObjectBase(L);
    lua_pushstring(L, "ZInput");   lua_setfield(L, -2, "ClassName");
    SET_DEFAULT_STR("InputType", "Text");
    SET_DEFAULT_TABLE("Choices");
    return 1;
}

static int s_mediaCounter = 0;

static int wherigo_ZMedia(lua_State *L) {
    lua_newtable(L);
    lua_pushstring(L, "ZMedia"); lua_setfield(L, -2, "ClassName");
    s_mediaCounter++;
    lua_pushinteger(L, s_mediaCounter); lua_setfield(L, -2, "MediaIndex");
    return 1;
}

void resetMediaCounter() { s_mediaCounter = 0; }

static int wherigo_ZCartridge(lua_State *L) {
    lua_newtable(L);
    lua_pushstring(L, "ZCartridge"); lua_setfield(L, -2, "ClassName");
    lua_newtable(L);                  lua_setfield(L, -2, "AllZObjects");
    lua_pushcfunction(L, wherigo_RequestSync); lua_setfield(L, -2, "RequestSync");
    return 1;
}

static int wherigo_ZCommand(lua_State *L) {
    lua_newtable(L);
    lua_pushstring(L, "ZCommand"); lua_setfield(L, -2, "ClassName");
    lua_pushboolean(L, 1);          lua_setfield(L, -2, "Enabled");
    return 1;
}

static int wherigo_Player(lua_State *L) {
    lua_newtable(L);
    lua_pushstring(L, "Player"); lua_setfield(L, -2, "ClassName");
    lua_pushstring(L, "Player"); lua_setfield(L, -2, "Name");
    lua_newtable(L);             lua_setfield(L, -2, "Inventory");
    lua_newtable(L);             lua_setfield(L, -2, "InsideOfZones");

    // ObjectLocation: ZonePoint(0,0,0)
    lua_newtable(L);
    lua_pushnumber(L, 0.0); lua_setfield(L, -2, "latitude");
    lua_pushnumber(L, 0.0); lua_setfield(L, -2, "longitude");
    lua_pushnumber(L, 0.0); lua_setfield(L, -2, "altitude");
    lua_setfield(L, -2, "ObjectLocation");

    pushDistance(L, 5.0); lua_setfield(L, -2, "PositionAccuracy");
    return 1;
}

// ── UI functions ──────────────────────────────────────────────────────────────

static int wherigo_MessageBox(lua_State *L) {
    if (!lua_istable(L, 1)) return 0;

    lua_getfield(L, 1, "Text");
    const char *text = lua_isstring(L, -1) ? lua_tostring(L, -1) : "";
    lua_pop(L, 1);

    // Media: ZMedia table → Name field (used for image display)
    std::string mediaName;
    lua_getfield(L, 1, "Media");
    if (lua_istable(L, -1)) {
        lua_getfield(L, -1, "Name");
        if (lua_isstring(L, -1)) mediaName = lua_tostring(L, -1);
        lua_pop(L, 1);
    }
    lua_pop(L, 1);

    std::vector<wxString> buttons;
    lua_getfield(L, 1, "Buttons");
    if (lua_istable(L, -1)) {
        const int n = lua_objlen(L, -1);
        for (int i = 1; i <= n; i++) {
            lua_rawgeti(L, -1, i);
            if (lua_isstring(L, -1))
                buttons.push_back(wxString::FromUTF8(lua_tostring(L, -1)));
            lua_pop(L, 1);
        }
    }
    lua_pop(L, 1);

    lua_getfield(L, 1, "Callback");
    const bool hasCallback = lua_isfunction(L, -1);
    int callbackRef = LUA_NOREF;
    if (hasCallback)
        callbackRef = luaL_ref(L, LUA_REGISTRYINDEX);
    else
        lua_pop(L, 1);

    wxLogDebug("MessageBox: %s", text);
    WherigoMessageDialog dlg(nullptr, wxString::FromUTF8(text), cartridgeTitle(), buttons,
                             wxString::FromUTF8(mediaName));
    dlg.ShowModal();
    const int selected = dlg.getSelectedButton();

    if (callbackRef != LUA_NOREF) {
        lua_rawgeti(L, LUA_REGISTRYINDEX, callbackRef);
        if (!buttons.empty() && selected >= 0 && selected < static_cast<int>(buttons.size()))
            lua_pushstring(L, buttons[selected].ToUTF8().data());
        else if (buttons.empty() && selected >= 0)
            lua_pushstring(L, "");  // OK clicked → "" keeps chain alive
        else
            lua_pushnil(L);  // window closed with X → nil breaks the chain
        if (lua_pcall(L, 1, 0, 0) != 0) {
            wxLogError("MessageBox callback error: %s", lua_tostring(L, -1));
            lua_pop(L, 1);
        }
        luaL_unref(L, LUA_REGISTRYINDEX, callbackRef);
    }

    GameEngine::getInstance().notifyStateChanged();
    return 0;
}

static int wherigo_Dialog(lua_State *L) {
    if (!lua_istable(L, 1)) return 0;

    std::vector<DialogEntry> entries;
    const int n = lua_objlen(L, 1);
    for (int i = 1; i <= n; i++) {
        lua_rawgeti(L, 1, i);
        if (lua_istable(L, -1)) {
            DialogEntry entry;
            lua_getfield(L, -1, "Text");
            entry.text = lua_isstring(L, -1) ? lua_tostring(L, -1) : "";
            lua_pop(L, 1);

            lua_getfield(L, -1, "Media");
            if (lua_istable(L, -1)) {
                lua_getfield(L, -1, "Name");
                entry.mediaName = lua_isstring(L, -1) ? lua_tostring(L, -1) : "";
                lua_pop(L, 1);
            }
            lua_pop(L, 1);

            lua_getfield(L, -1, "Buttons");
            if (lua_istable(L, -1)) {
                const int bn = lua_objlen(L, -1);
                for (int j = 1; j <= bn; j++) {
                    lua_rawgeti(L, -1, j);
                    if (lua_isstring(L, -1)) entry.buttons.push_back(lua_tostring(L, -1));
                    lua_pop(L, 1);
                }
            }
            lua_pop(L, 1);
            entries.push_back(entry);
        }
        lua_pop(L, 1);
    }

    for (const auto &e : entries)
        wxLogDebug("Dialog: %s", e.text.c_str());

    WherigoDialogRunner::getInstance().showDialog(entries);
    return 0;
}

static int wherigo_GetInput(lua_State *L) {
    if (!lua_istable(L, 1)) return 0;

    lua_getfield(L, 1, "InputType");
    const std::string inputType = lua_isstring(L,-1) ? lua_tostring(L,-1) : "Text";
    lua_pop(L, 1);

    lua_getfield(L, 1, "Text");
    const wxString prompt = lua_isstring(L,-1)
        ? wxString::FromUTF8(lua_tostring(L,-1)) : wxString("Eingabe:");
    lua_pop(L, 1);

    lua_getfield(L, 1, "Name");
    const wxString inputName = lua_isstring(L,-1)
        ? wxString::FromUTF8(lua_tostring(L,-1)) : wxString("(unnamed)");
    lua_pop(L, 1);

    // Debug: log question text
    wxLogDebug("GetInput[%s] Frage: %s", inputType, prompt);

    std::string response;

    if (inputType == "MultipleChoice") {
        wxArrayString choices;
        lua_getfield(L, 1, "Choices");
        if (lua_istable(L, -1)) {
            const int n = lua_objlen(L, -1);
            for (int i = 1; i <= n; i++) {
                lua_rawgeti(L, -1, i);
                if (lua_isstring(L, -1)) {
                    wxString c = wxString::FromUTF8(lua_tostring(L,-1));
                    choices.Add(c);
                    wxLogDebug("GetInput  Option: %s", c);
                }
                lua_pop(L, 1);
            }
        }
        lua_pop(L, 1);
        if (!choices.IsEmpty()) {
            wxSingleChoiceDialog dlg(nullptr, prompt, cartridgeTitle(), choices);
            if (dlg.ShowModal() == wxID_OK)
                response = dlg.GetStringSelection().ToUTF8().data();
        }
    } else {
        wxLogDebug("GetInput  Freitext (Name: %s)", inputName);
        wxTextEntryDialog dlg(nullptr, prompt, cartridgeTitle(), "");
        if (dlg.ShowModal() == wxID_OK)
            response = dlg.GetValue().ToUTF8().data();
    }

    lua_getfield(L, 1, "OnGetInput");
    if (lua_isfunction(L, -1)) {
        lua_pushvalue(L, 1);
        lua_pushstring(L, response.c_str());
        if (lua_pcall(L, 2, 0, 0) != 0) {
            wxLogError("GetInput OnGetInput error: %s", lua_tostring(L,-1));
            lua_pop(L, 1);
        }
    } else {
        lua_pop(L, 1);
    }

    // OnGetInput may have changed item visibility → rebuild inventory
    GameEngine::getInstance().rebuildPlayerInventory();
    GameEngine::getInstance().notifyStateChanged();
    return 0;
}

// ── Audio playback ────────────────────────────────────────────────────────────

static long    s_audioPid  = 0;
static wxString s_audioTemp;

static void stopAudio() {
    if (s_audioPid > 0) {
        wxProcess::Kill(s_audioPid, wxSIGKILL, wxKILL_CHILDREN);
        s_audioPid = 0;
    }
    if (!s_audioTemp.empty()) {
        wxRemoveFile(s_audioTemp);
        s_audioTemp.clear();
    }
}

static void playAudio(const std::vector<uint8_t>& data, const std::string& ext) {
    stopAudio();

    static int s_counter = 0;
    s_audioTemp = wxFileName::GetTempDir() +
        wxFileName::GetPathSeparator() +
        wxString::Format("wherigo_%d.%s", s_counter++,
                         wxString::FromUTF8(ext).Lower());

    {
        wxFile f(s_audioTemp, wxFile::write);
        if (!f.IsOpened()) {
            wxLogWarning("PlayAudio: cannot write temp file %s", s_audioTemp);
            s_audioTemp.clear();
            return;
        }
        f.Write(data.data(), data.size());
    }

#if defined(__APPLE__)
    const wxString cmd = wxString::Format("afplay \"%s\"", s_audioTemp);
#elif defined(__linux__)
    const wxString cmd = wxString::Format(
        "mpg123 -q \"%s\" 2>/dev/null || "
        "ffplay -nodisp -autoexit -loglevel quiet \"%s\"",
        s_audioTemp, s_audioTemp);
#else
    const wxString cmd = wxString::Format(
        "powershell -c \"Add-Type -AssemblyName presentationCore;"
        "$mp=[Windows.Media.Playback.MediaPlayer]::new();"
        "$mp.Source=[Windows.Media.Core.MediaSource]::CreateFromUri('%s');"
        "$mp.Play();Start-Sleep -s 300\"",
        s_audioTemp);
#endif

    wxLogDebug("PlayAudio: launching %s", cmd);
    s_audioPid = wxExecute(cmd, wxEXEC_ASYNC);
    if (s_audioPid <= 0) {
        wxLogWarning("PlayAudio: failed to start player");
        wxRemoveFile(s_audioTemp);
        s_audioTemp.clear();
        s_audioPid = 0;
    }
}

static int wherigo_PlayAudio(lua_State *L) {
    if (!lua_istable(L, 1)) return 0;

    lua_getfield(L, 1, "Name");
    wxLogDebug("PlayAudio: %s", lua_isstring(L,-1) ? lua_tostring(L,-1) : "(unnamed)");
    lua_pop(L, 1);

    // MediaIndex assigned by wherigo_ZMedia at construction time
    lua_getfield(L, 1, "MediaIndex");
    const int mediaIndex = lua_isnumber(L,-1) ? lua_tointeger(L,-1) : -1;
    lua_pop(L, 1);
    if (mediaIndex <= 0) return 0;

    // File extension from Resources[1].Type (set by cartridge code)
    std::string ext = "mp3";
    lua_getfield(L, 1, "Resources");
    if (lua_istable(L, -1)) {
        lua_rawgeti(L, -1, 1);
        if (lua_istable(L, -1)) {
            lua_getfield(L, -1, "Type");
            if (lua_isstring(L, -1)) ext = lua_tostring(L, -1);
            lua_pop(L, 1);
        }
        lua_pop(L, 1);
    }
    lua_pop(L, 1);

    const auto data = MediaManager::getInstance().getMediaByIndex(mediaIndex);
    if (data.empty()) {
        wxLogWarning("PlayAudio: no data for MediaIndex %d", mediaIndex);
        return 0;
    }

    playAudio(data, ext);
    return 0;
}

static int wherigo_ShowScreen(lua_State *L) {
    const int screen = luaL_optinteger(L, 1, 0);
    wxLogDebug("ShowScreen: %d", screen);

    if (screen == 5 && lua_gettop(L) >= 2 && lua_istable(L, 2)) {
        lua_getfield(L, 2, "Name");
        const std::string name = lua_isstring(L,-1) ? lua_tostring(L,-1) : "Details";
        lua_pop(L, 1);
        lua_getfield(L, 2, "Description");
        const std::string desc = lua_isstring(L,-1) ? lua_tostring(L,-1) : "";
        lua_pop(L, 1);

        std::string mediaName;
        lua_getfield(L, 2, "Media");
        if (lua_istable(L, -1)) {
            lua_getfield(L, -1, "Name");
            if (lua_isstring(L, -1)) mediaName = lua_tostring(L, -1);
            lua_pop(L, 1);
        }
        lua_pop(L, 1);
        if (mediaName.empty()) {
            lua_getfield(L, 2, "Icon");
            if (lua_istable(L, -1)) {
                lua_getfield(L, -1, "Name");
                if (lua_isstring(L, -1)) mediaName = lua_tostring(L, -1);
                lua_pop(L, 1);
            }
            lua_pop(L, 1);
        }
        WherigoMessageDialog dlg(nullptr,
            wxString::FromUTF8(desc), wxString::FromUTF8(name),
            {"OK"}, wxString::FromUTF8(mediaName));
        dlg.ShowModal();
    }
    return 0;
}

static int wherigo_ShowStatusText(lua_State *L) {
    const char *text = luaL_checkstring(L, 1);
    wxLogDebug("ShowStatusText: %s", text);
    return 0;
}

static int wherigo_LogMessage(lua_State *L) {
    const char *msg = luaL_checkstring(L, 1);
    wxLogDebug("Wherigo.LogMessage: %s", msg);
    return 0;
}

static int wherigo_Command(lua_State *L) {
    const char *text = luaL_checkstring(L, 1);
    wxLogDebug("Wherigo.Command: %s", text);
    if (!strcmp(text, "SaveClose")) {
        wxLogMessage("Wherigo.Command: SaveClose – exiting");
        wxTheApp->ExitMainLoop();
    } else if (!strcmp(text, "StopSound")) {
        stopAudio();
    } else if (!strcmp(text, "Alert")) {
        wxBell();
    }
    return 0;
}

// ── String utilities ──────────────────────────────────────────────────────────

static int wherigo_NoCaseEquals(lua_State *L) {
    const char *s1 = luaL_checkstring(L, 1);
    const char *s2 = luaL_checkstring(L, 2);
    std::string a(s1), b(s2);
    std::transform(a.begin(), a.end(), a.begin(), ::tolower);
    std::transform(b.begin(), b.end(), b.begin(), ::tolower);
    lua_pushboolean(L, a == b);
    return 1;
}

// ── Geo functions ─────────────────────────────────────────────────────────────

static int wherigo_IsPointInZone(lua_State *L) {
    if (!lua_istable(L, 1) || !lua_istable(L, 2)) {
        lua_pushboolean(L, 0);
        return 1;
    }
    lua_getfield(L, 1, "latitude");  const double lat = lua_tonumber(L,-1); lua_pop(L,1);
    lua_getfield(L, 1, "longitude"); const double lon = lua_tonumber(L,-1); lua_pop(L,1);
    const auto pts = readZonePoints(L, 2);
    lua_pushboolean(L, pointInPolygon(lat, lon, pts) ? 1 : 0);
    return 1;
}

static int wherigo_VectorToPoint(lua_State *L) {
    if (!lua_istable(L, 1) || !lua_istable(L, 2)) {
        pushDistance(L, 0.0); pushBearing(L, 0.0); return 2;
    }
    lua_getfield(L, 1, "latitude");  const double lat1 = lua_tonumber(L,-1); lua_pop(L,1);
    lua_getfield(L, 1, "longitude"); const double lon1 = lua_tonumber(L,-1); lua_pop(L,1);
    lua_getfield(L, 2, "latitude");  const double lat2 = lua_tonumber(L,-1); lua_pop(L,1);
    lua_getfield(L, 2, "longitude"); const double lon2 = lua_tonumber(L,-1); lua_pop(L,1);
    const auto [dist, brg] = haversine(lat1, lon1, lat2, lon2);
    pushDistance(L, dist);
    pushBearing(L, brg);
    return 2;
}

static int wherigo_VectorToSegment(lua_State *L) {
    if (!lua_istable(L, 1) || !lua_istable(L, 2) || !lua_istable(L, 3)) {
        pushDistance(L, 0.0); pushBearing(L, 0.0); return 2;
    }
    lua_getfield(L, 1, "latitude");  const double plat = lua_tonumber(L,-1); lua_pop(L,1);
    lua_getfield(L, 1, "longitude"); const double plon = lua_tonumber(L,-1); lua_pop(L,1);
    lua_getfield(L, 2, "latitude");  const double lat1 = lua_tonumber(L,-1); lua_pop(L,1);
    lua_getfield(L, 2, "longitude"); const double lon1 = lua_tonumber(L,-1); lua_pop(L,1);
    lua_getfield(L, 3, "latitude");  const double lat2 = lua_tonumber(L,-1); lua_pop(L,1);
    lua_getfield(L, 3, "longitude"); const double lon2 = lua_tonumber(L,-1); lua_pop(L,1);
    double nLat, nLon;
    const double dist = distToSegment(plat, plon, lat1, lon1, lat2, lon2, &nLat, &nLon);
    const double brg  = haversine(plat, plon, nLat, nLon).second;
    pushDistance(L, dist);
    pushBearing(L, brg);
    return 2;
}

static int wherigo_VectorToZone(lua_State *L) {
    if (!lua_istable(L, 1) || !lua_istable(L, 2)) {
        pushDistance(L, 0.0); pushBearing(L, 0.0); return 2;
    }
    lua_getfield(L, 1, "latitude");  const double plat = lua_tonumber(L,-1); lua_pop(L,1);
    lua_getfield(L, 1, "longitude"); const double plon = lua_tonumber(L,-1); lua_pop(L,1);
    const auto pts = readZonePoints(L, 2);
    if (pointInPolygon(plat, plon, pts)) {
        pushDistance(L, 0.0); pushBearing(L, 0.0); return 2;
    }
    const auto [dist, brg] = nearestZoneEdge(plat, plon, pts);
    pushDistance(L, dist);
    pushBearing(L, brg);
    return 2;
}

static int wherigo_TranslatePoint(lua_State *L) {
    if (!lua_istable(L, 1)) { lua_pushnil(L); return 1; }

    lua_getfield(L, 1, "latitude");  const double lat = lua_tonumber(L,-1); lua_pop(L,1);
    lua_getfield(L, 1, "longitude"); const double lon = lua_tonumber(L,-1); lua_pop(L,1);
    lua_getfield(L, 1, "altitude");
    const double alt = lua_isnumber(L,-1) ? lua_tonumber(L,-1) : 0.0;
    lua_pop(L, 1);

    double distMeters = 0.0;
    if (lua_istable(L, 2)) {
        lua_getfield(L, 2, "value"); distMeters = lua_tonumber(L,-1); lua_pop(L,1);
    } else if (lua_isnumber(L, 2)) {
        distMeters = lua_tonumber(L, 2);
    }

    double bearingDeg = 0.0;
    if (lua_istable(L, 3)) {
        lua_getfield(L, 3, "value"); bearingDeg = lua_tonumber(L,-1); lua_pop(L,1);
    } else if (lua_isnumber(L, 3)) {
        bearingDeg = lua_tonumber(L, 3);
    }

    const auto [newLat, newLon] = translatePoint(lat, lon, distMeters, bearingDeg);
    lua_newtable(L);
    lua_pushnumber(L, newLat);        lua_setfield(L, -2, "latitude");
    lua_pushnumber(L, newLon);        lua_setfield(L, -2, "longitude");
    lua_pushnumber(L, alt);           lua_setfield(L, -2, "altitude");
    lua_pushstring(L, "ZonePoint");   lua_setfield(L, -2, "_classname");
    return 1;
}

// ── Function table & module init ──────────────────────────────────────────────

static const luaL_Reg wherigo_funcs[] = {
    {"ZonePoint",      wherigo_ZonePoint},
    {"Distance",       wherigo_Distance},
    {"Bearing",        wherigo_Bearing},
    {"Zone",           wherigo_Zone},
    {"ZCharacter",     wherigo_ZCharacter},
    {"ZItem",          wherigo_ZItem},
    {"ZTask",          wherigo_ZTask},
    {"ZTimer",         wherigo_ZTimer},
    {"ZInput",         wherigo_ZInput},
    {"ZMedia",         wherigo_ZMedia},
    {"ZCartridge",     wherigo_ZCartridge},
    {"ZCommand",       wherigo_ZCommand},
    {"MessageBox",     wherigo_MessageBox},
    {"Dialog",         wherigo_Dialog},
    {"GetInput",       wherigo_GetInput},
    {"PlayAudio",      wherigo_PlayAudio},
    {"ShowScreen",     wherigo_ShowScreen},
    {"ShowStatusText", wherigo_ShowStatusText},
    {"LogMessage",     wherigo_LogMessage},
    {"Command",        wherigo_Command},
    {"NoCaseEquals",   wherigo_NoCaseEquals},
    {"IsPointInZone",  wherigo_IsPointInZone},
    {"TranslatePoint", wherigo_TranslatePoint},
    {"VectorToZone",   wherigo_VectorToZone},
    {"VectorToPoint",  wherigo_VectorToPoint},
    {"VectorToSegment",wherigo_VectorToSegment},
    {"RequestSync",    wherigo_RequestSync},
    {nullptr, nullptr}
};

int luaopen_Wherigo(lua_State *L) {
    // Register metatables before any object constructors are used
    setupMetatables(L);

    luaL_register(L, "Wherigo", wherigo_funcs);

    // Player object
    // One Player table, referenced by both Wherigo.Player and the global Player
    wherigo_Player(L);                 // push Player table
    lua_pushvalue(L, -1);              // duplicate
    lua_setfield(L, -3, "Player");     // Wherigo.Player = table (pops duplicate)
    lua_setglobal(L, "Player");        // Player (global) = same table (pops original)

    // Screen constants (numbers matching original Groundspeak spec)
    lua_pushnumber(L, 0); lua_setfield(L, -2, "MAINSCREEN");
    lua_pushnumber(L, 1); lua_setfield(L, -2, "LOCATIONSCREEN");
    lua_pushnumber(L, 2); lua_setfield(L, -2, "ITEMSCREEN");
    lua_pushnumber(L, 3); lua_setfield(L, -2, "INVENTORYSCREEN");
    lua_pushnumber(L, 4); lua_setfield(L, -2, "TASKSCREEN");
    lua_pushnumber(L, 5); lua_setfield(L, -2, "DETAILSCREEN");

    // Log level constants
    lua_pushnumber(L, 150); lua_setfield(L, -2, "LOGDEBUG");
    lua_pushnumber(L, 151); lua_setfield(L, -2, "LOGCARTRIDGE");
    lua_pushnumber(L, 152); lua_setfield(L, -2, "LOGINFO");
    lua_pushnumber(L, 153); lua_setfield(L, -2, "LOGWARNING");
    lua_pushnumber(L, 154); lua_setfield(L, -2, "LOGERROR");

    // Class name constants
    lua_pushstring(L, "Zone");       lua_setfield(L, -2, "CLASS_ZONE");
    lua_pushstring(L, "ZMedia");     lua_setfield(L, -2, "CLASS_ZMEDIA");
    lua_pushstring(L, "ZCartridge"); lua_setfield(L, -2, "CLASS_ZCARTRIDGE");
    lua_pushstring(L, "ZCharacter"); lua_setfield(L, -2, "CLASS_ZCHARACTER");
    lua_pushstring(L, "ZCommand");   lua_setfield(L, -2, "CLASS_ZCOMMAND");
    lua_pushstring(L, "ZInput");     lua_setfield(L, -2, "CLASS_ZINPUT");
    lua_pushstring(L, "ZTask");      lua_setfield(L, -2, "CLASS_ZTASK");
    lua_pushstring(L, "ZItem");      lua_setfield(L, -2, "CLASS_ZITEM");
    lua_pushstring(L, "ZTimer");     lua_setfield(L, -2, "CLASS_ZTIMER");
    lua_pushstring(L, "Distance");   lua_setfield(L, -2, "CLASS_DISTANCE");
    lua_pushstring(L, "Bearing");    lua_setfield(L, -2, "CLASS_BEARING");
    lua_pushstring(L, "ZonePoint");  lua_setfield(L, -2, "CLASS_ZONEPOINT");

    // INVALID_ZONEPOINT
    lua_pushnil(L); lua_setfield(L, -2, "INVALID_ZONEPOINT");

    // Register globals expected by Wherigo Builder output
    lua_pushcfunction(L, wherigo_ZonePoint); lua_setglobal(L, "ZonePoint");
    lua_pushcfunction(L, wherigo_Distance);  lua_setglobal(L, "Distance");

    return 1;
}

} // namespace wherigo
