#include "lua/game_engine.h"
#include "lua/geo_utils.h"

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

#include <wx/log.h>

#include <string>
#include <vector>

namespace wherigo {

wxDEFINE_EVENT(EVT_GAME_STATE_CHANGED, GameStateEvent);

wxBEGIN_EVENT_TABLE(GameEngine, wxEvtHandler)
  EVT_TIMER(wxID_ANY, GameEngine::onGameTick)
wxEND_EVENT_TABLE()

GameEngine& GameEngine::getInstance() {
  static GameEngine instance;
  return instance;
}

GameEngine::GameEngine() : m_gameTimer(this) {}
GameEngine::~GameEngine() { shutdown(); }

void GameEngine::init(lua_State *L, int cartridgeRef) {
  m_luaState = L;
  m_cartridgeRef = cartridgeRef;
  wxLogDebug("GameEngine initialized");
}

void GameEngine::shutdown() {
  stop();
  m_luaState = nullptr;
  m_cartridgeRef = -1;
}

void GameEngine::start() {
  if (m_running) return;
  m_running = true;
  m_gameTimer.Start(1000);
  wxLogDebug("GameEngine started");
}

void GameEngine::stop() {
  if (!m_running) return;
  m_gameTimer.Stop();
  m_running = false;
  wxLogDebug("GameEngine stopped");
}

void GameEngine::updatePlayerPosition(double lat, double lng, double alt) {
  m_playerLat = lat;
  m_playerLng = lng;
  m_playerAlt = alt;

  if (!m_luaState) return;

  // Update Player.ObjectLocation in Lua
  lua_getglobal(m_luaState, "Player");
  if (lua_istable(m_luaState, -1)) {
    lua_newtable(m_luaState);
    lua_pushnumber(m_luaState, lat); lua_setfield(m_luaState, -2, "latitude");
    lua_pushnumber(m_luaState, lng); lua_setfield(m_luaState, -2, "longitude");
    lua_pushnumber(m_luaState, alt); lua_setfield(m_luaState, -2, "altitude");
    lua_setfield(m_luaState, -2, "ObjectLocation");
  }
  lua_pop(m_luaState, 1);

  if (!m_isProcessing) {
    m_isProcessing = true;
    checkZones();
    notifyStateChanged();
    m_isProcessing = false;
  }
}

void GameEngine::onGameTick(wxTimerEvent&) {
  if (!m_luaState || !m_running || m_isProcessing) return;
  m_isProcessing = true;
  checkTimers();
  notifyStateChanged();
  m_isProcessing = false;
}

// ── Timer processing ──────────────────────────────────────────────────────────

void GameEngine::checkTimers() {
  if (!m_luaState) return;

  lua_getglobal(m_luaState, "_G");
  lua_pushnil(m_luaState);

  while (lua_next(m_luaState, -2) != 0) {
    if (lua_istable(m_luaState, -1)) {
      lua_getfield(m_luaState, -1, "ClassName");
      const bool isTimer = lua_isstring(m_luaState, -1) &&
                           !strcmp(lua_tostring(m_luaState, -1), "ZTimer");
      lua_pop(m_luaState, 1);

      if (isTimer) {
        lua_getfield(m_luaState, -1, "Running");
        const bool running = lua_toboolean(m_luaState, -1);
        lua_pop(m_luaState, 1);

        if (running) {
          lua_getfield(m_luaState, -1, "Name");
          const char *name = lua_isstring(m_luaState,-1) ? lua_tostring(m_luaState,-1) : "(unnamed)";
          lua_pop(m_luaState, 1);

          lua_getfield(m_luaState, -1, "Remaining");
          double remaining = lua_isnumber(m_luaState,-1) ? lua_tonumber(m_luaState,-1) : -1.0;
          lua_pop(m_luaState, 1);

          if (remaining < 0) {
            lua_getfield(m_luaState, -1, "Duration");
            remaining = lua_isnumber(m_luaState,-1) ? lua_tonumber(m_luaState,-1) : 0.0;
            lua_pop(m_luaState, 1);
          }

          remaining -= 1.0;
          lua_pushnumber(m_luaState, remaining);
          lua_setfield(m_luaState, -2, "Remaining");

          lua_getfield(m_luaState, -1, "OnTick");
          if (lua_isfunction(m_luaState, -1)) {
            lua_pushvalue(m_luaState, -2);
            if (lua_pcall(m_luaState, 1, 0, 0) != 0) {
              wxLogError("Timer OnTick error: %s", lua_tostring(m_luaState,-1));
              lua_pop(m_luaState, 1);
            }
          } else {
            lua_pop(m_luaState, 1);
          }

          if (remaining <= 0) {
            wxLogDebug("Timer elapsed: %s", name);
            lua_pushboolean(m_luaState, 0);
            lua_setfield(m_luaState, -2, "Running");

            lua_getfield(m_luaState, -1, "OnElapsed");
            if (lua_isfunction(m_luaState, -1)) {
              lua_pushvalue(m_luaState, -2);
              if (lua_pcall(m_luaState, 1, 0, 0) != 0) {
                wxLogError("Timer OnElapsed error: %s", lua_tostring(m_luaState,-1));
                lua_pop(m_luaState, 1);
              }
            } else {
              lua_pop(m_luaState, 1);
            }
          }
        }
      }
    }
    lua_pop(m_luaState, 1);
  }
  lua_pop(m_luaState, 1);
}

// ── Zone processing ───────────────────────────────────────────────────────────

// Fire a named callback on a zone (absolute index zoneIdx). Stack-neutral.
static void callZoneCallback(lua_State *L, int zoneIdx,
                             const char *cbName, const char *zoneName) {
  lua_getfield(L, zoneIdx, cbName);
  if (lua_isfunction(L, -1)) {
    wxLogDebug("Zone <%s>: START %s", zoneName, cbName);
    lua_pushvalue(L, zoneIdx);
    if (lua_pcall(L, 1, 0, 0) != 0) {
      wxLogError("Zone %s error: %s", cbName, lua_tostring(L,-1));
      lua_pop(L, 1);
    }
    wxLogDebug("Zone <%s>: END__ %s", zoneName, cbName);
  } else {
    lua_pop(L, 1);
  }
}

// Process one zone table at absolute stack index. Returns true if state changed.
static bool updateZone(lua_State *L, int zoneIdx, double playerLat, double playerLng) {
  // Zone name (for logging)
  lua_getfield(L, zoneIdx, "Name");
  const std::string zoneName = lua_isstring(L,-1) ? lua_tostring(L,-1) : "(unnamed)";
  lua_pop(L, 1);

  // Need at least 3 points for a polygon
  const auto pts = readZonePoints(L, zoneIdx);
  if (pts.size() < 3) return false;

  // Previous state
  lua_getfield(L, zoneIdx, "_inside");
  bool wasInside = lua_toboolean(L, -1);
  lua_pop(L, 1);

  lua_getfield(L, zoneIdx, "_state");
  const std::string prevState = lua_isstring(L,-1) ? lua_tostring(L,-1) : "NotInRange";
  lua_pop(L, 1);

  // Proximity / distance range thresholds
  double proximityRange = 60.0;
  lua_getfield(L, zoneIdx, "ProximityRange");
  if (lua_istable(L, -1)) {
    lua_getfield(L, -1, "value");
    if (lua_isnumber(L,-1)) proximityRange = lua_tonumber(L,-1);
    lua_pop(L, 1);
  }
  lua_pop(L, 1);

  double distanceRange = -0.3048;
  lua_getfield(L, zoneIdx, "DistanceRange");
  if (lua_istable(L, -1)) {
    lua_getfield(L, -1, "value");
    if (lua_isnumber(L,-1)) distanceRange = lua_tonumber(L,-1);
    lua_pop(L, 1);
  }
  lua_pop(L, 1);

  // Current position in zone?
  bool inside = pointInPolygon(playerLat, playerLng, pts);

  // Distance / bearing to zone boundary
  double currentDist = 0.0, currentBrg = 0.0;
  if (!inside) {
    const auto [d, b] = nearestZoneEdge(playerLat, playerLng, pts);
    currentDist = d;
    currentBrg  = b;
  }

  // Update zone fields
  pushDistance(L, inside ? 0.0 : currentDist);
  lua_setfield(L, zoneIdx, "CurrentDistance");
  pushBearing(L, inside ? 0.0 : currentBrg);
  lua_setfield(L, zoneIdx, "CurrentBearing");

  constexpr double COMFORT = 7.0; // meters hysteresis buffer
  bool stateChanged = false;

  // ── Handle enter / exit transitions ────────────────────────────────────────
  if (inside != wasInside) {
    if (inside) {
      // Entering the zone
      if (prevState == "NotInRange")
        callZoneCallback(L, zoneIdx, "OnDistant", zoneName.c_str());
      if (prevState != "Proximity" && prevState != "Inside")
        callZoneCallback(L, zoneIdx, "OnProximity", zoneName.c_str());
      callZoneCallback(L, zoneIdx, "OnEnter", zoneName.c_str());
      stateChanged = true;
    } else if (currentDist > COMFORT) {
      // Confirmed exit (beyond hysteresis buffer)
      callZoneCallback(L, zoneIdx, "OnExit", zoneName.c_str());
      stateChanged = true;
    } else {
      // Within comfort zone – stay inside
      inside = true;
    }
    lua_pushboolean(L, inside ? 1 : 0);
    lua_setfield(L, zoneIdx, "_inside");
  }

  // ── Determine new state string ──────────────────────────────────────────────
  std::string newState;
  if (inside) {
    newState = "Inside";
  } else if (currentDist < (proximityRange + COMFORT)) {
    newState = "Proximity";
  } else if (distanceRange < 0.0 || currentDist < (distanceRange + COMFORT)) {
    newState = "Distant";
  } else {
    newState = "NotInRange";
  }

  // Fire state-change callbacks for non-inside transitions
  if (!inside && newState != prevState) {
    stateChanged = true;
    if (prevState == "Inside") {
      callZoneCallback(L, zoneIdx, "OnProximity", zoneName.c_str());
      if (newState == "NotInRange")
        callZoneCallback(L, zoneIdx, "OnDistant", zoneName.c_str());
    } else if (prevState == "Proximity" && newState == "NotInRange") {
      callZoneCallback(L, zoneIdx, "OnDistant", zoneName.c_str());
    } else if (prevState == "NotInRange" && newState == "Proximity") {
      callZoneCallback(L, zoneIdx, "OnDistant", zoneName.c_str());
    }
  }

  lua_pushstring(L, newState.c_str()); lua_setfield(L, zoneIdx, "State");
  lua_pushstring(L, newState.c_str()); lua_setfield(L, zoneIdx, "_state");

  return stateChanged;
}

void GameEngine::checkZones() {
  if (!m_luaState) return;

  // Collect all active zone references first (to avoid lua_next invalidation)
  std::vector<int> zoneRefs;
  lua_getglobal(m_luaState, "_G");
  lua_pushnil(m_luaState);
  while (lua_next(m_luaState, -2) != 0) {
    if (lua_istable(m_luaState, -1)) {
      lua_getfield(m_luaState, -1, "ClassName");
      const bool isZone = lua_isstring(m_luaState,-1) &&
                          !strcmp(lua_tostring(m_luaState,-1), "Zone");
      lua_pop(m_luaState, 1);
      if (isZone) {
        lua_getfield(m_luaState, -1, "Active");
        const bool active = lua_toboolean(m_luaState, -1);
        lua_pop(m_luaState, 1);
        if (active) {
          lua_pushvalue(m_luaState, -1);
          zoneRefs.push_back(luaL_ref(m_luaState, LUA_REGISTRYINDEX));
        }
      }
    }
    lua_pop(m_luaState, 1);
  }
  lua_pop(m_luaState, 1);

  // Process each zone
  bool anyChanged = false;
  for (const int ref : zoneRefs) {
    lua_rawgeti(m_luaState, LUA_REGISTRYINDEX, ref);
    if (updateZone(m_luaState, lua_gettop(m_luaState), m_playerLat, m_playerLng))
      anyChanged = true;
    lua_pop(m_luaState, 1);
    luaL_unref(m_luaState, LUA_REGISTRYINDEX, ref);
  }

  if (anyChanged) {
    rebuildPlayerInventory();
    notifyStateChanged();
  }
}

void GameEngine::rebuildPlayerInventory() {
  if (!m_luaState) return;

  // Player table
  lua_getglobal(m_luaState, "Player");
  if (!lua_istable(m_luaState, -1)) { lua_pop(m_luaState, 1); return; }
  const int playerIdx = lua_gettop(m_luaState);

  // New inventory array (only visible items with Container == Player)
  lua_newtable(m_luaState);
  const int invIdx = lua_gettop(m_luaState);
  int count = 0;

  lua_getglobal(m_luaState, "_G");
  const int gIdx = lua_gettop(m_luaState);
  lua_pushnil(m_luaState);

  while (lua_next(m_luaState, gIdx) != 0) {
    if (lua_istable(m_luaState, -1)) {
      lua_getfield(m_luaState, -1, "ClassName");
      const bool isItem = lua_isstring(m_luaState, -1) &&
                          !strcmp(lua_tostring(m_luaState, -1), "ZItem");
      lua_pop(m_luaState, 1);

      if (isItem) {
        lua_getfield(m_luaState, -1, "Container");
        const bool inInventory = lua_rawequal(m_luaState, -1, playerIdx);
        lua_pop(m_luaState, 1);

        if (inInventory) {
          lua_getfield(m_luaState, -1, "Visible");
          const bool visible = lua_toboolean(m_luaState, -1);
          lua_pop(m_luaState, 1);

          if (visible) {
            lua_pushvalue(m_luaState, -1);
            lua_rawseti(m_luaState, invIdx, ++count);
          }
        }
      }
    }
    lua_pop(m_luaState, 1); // pop value
  }
  lua_pop(m_luaState, 1); // pop _G

  lua_pushvalue(m_luaState, invIdx);
  lua_setfield(m_luaState, playerIdx, "Inventory");
  lua_pop(m_luaState, 2); // pop inventory + Player
}

void GameEngine::notifyStateChanged() {
  GameStateEvent event(EVT_GAME_STATE_CHANGED);
  ProcessEvent(event);
}

} // namespace wherigo
