#include "LuaEventApi.hpp"
#include "LuaEntityApi.hpp"
#include "../systems/EventBus.hpp"
#include "../components/EventParams.hpp"
#include <OgreLogManager.h>
#include <unordered_map>

namespace editScene
{

// ---------------------------------------------------------------------------
// Internal: subscription ID tracking for Lua-managed subscriptions
// ---------------------------------------------------------------------------

/**
 * @brief Map from Lua subscription IDs to EventBus ListenerIds.
 *
 * When Lua subscribes to an event, we store the mapping so that
 * unsubscribe_event() can remove the correct listener.
 */
static std::unordered_map<int, EventBus::ListenerId> s_luaSubscriptions;
static int s_nextLuaSubId = 1;

// ---------------------------------------------------------------------------
// Helper: push an EventValue as a Lua value
// ---------------------------------------------------------------------------

/**
 * @brief Push a single EventValue onto the Lua stack.
 *
 * @param L   Lua state.
 * @param val The EventValue to push.
 */
static void pushEventValue(lua_State *L, const EventValue &val)
{
	switch (val.getType()) {
	case EventValue::NIL:
		lua_pushnil(L);
		break;
	case EventValue::ENTITY_ID:
		lua_pushinteger(L, (lua_Integer)val.getEntityId());
		break;
	case EventValue::INT:
		lua_pushinteger(L, (lua_Integer)val.getInt());
		break;
	case EventValue::FLOAT:
		lua_pushnumber(L, (lua_Number)val.getFloat());
		break;
	case EventValue::DOUBLE:
		lua_pushnumber(L, (lua_Number)val.getDouble());
		break;
	case EventValue::STRING:
		lua_pushstring(L, val.getString().c_str());
		break;
	case EventValue::ENTITY_ID_ARRAY: {
		lua_newtable(L);
		const auto &arr = val.getEntityIdArray();
		for (size_t i = 0; i < arr.size(); i++) {
			lua_pushinteger(L, (lua_Integer)arr[i]);
			lua_rawseti(L, -2, (int)(i + 1));
		}
		break;
	}
	case EventValue::INT_ARRAY: {
		lua_newtable(L);
		const auto &arr = val.getIntArray();
		for (size_t i = 0; i < arr.size(); i++) {
			lua_pushinteger(L, (lua_Integer)arr[i]);
			lua_rawseti(L, -2, (int)(i + 1));
		}
		break;
	}
	case EventValue::FLOAT_ARRAY: {
		lua_newtable(L);
		const auto &arr = val.getFloatArray();
		for (size_t i = 0; i < arr.size(); i++) {
			lua_pushnumber(L, (lua_Number)arr[i]);
			lua_rawseti(L, -2, (int)(i + 1));
		}
		break;
	}
	case EventValue::DOUBLE_ARRAY: {
		lua_newtable(L);
		const auto &arr = val.getDoubleArray();
		for (size_t i = 0; i < arr.size(); i++) {
			lua_pushnumber(L, (lua_Number)arr[i]);
			lua_rawseti(L, -2, (int)(i + 1));
		}
		break;
	}
	case EventValue::STRING_ARRAY: {
		lua_newtable(L);
		const auto &arr = val.getStringArray();
		for (size_t i = 0; i < arr.size(); i++) {
			lua_pushstring(L, arr[i].c_str());
			lua_rawseti(L, -2, (int)(i + 1));
		}
		break;
	}
	}
}

/**
 * @brief Push the type name string for an EventValue type.
 */
static const char *eventValueTypeName(EventValue::Type type)
{
	switch (type) {
	case EventValue::NIL:
		return "nil";
	case EventValue::ENTITY_ID:
		return "entity_id";
	case EventValue::INT:
		return "int";
	case EventValue::FLOAT:
		return "float";
	case EventValue::DOUBLE:
		return "double";
	case EventValue::STRING:
		return "string";
	case EventValue::ENTITY_ID_ARRAY:
		return "entity_id_array";
	case EventValue::INT_ARRAY:
		return "int_array";
	case EventValue::FLOAT_ARRAY:
		return "float_array";
	case EventValue::DOUBLE_ARRAY:
		return "double_array";
	case EventValue::STRING_ARRAY:
		return "string_array";
	}
	return "nil";
}

// ---------------------------------------------------------------------------
// Helper: push an EventParams as a Lua table
// ---------------------------------------------------------------------------

/**
 * @brief Push an EventParams as a Lua table with named fields.
 *
 * The resulting table has each key mapped to its value, plus a _types
 * sub-table that maps each key to its type name string.
 *
 * @param L    Lua state.
 * @param params The EventParams to convert.
 */
static void pushEventParams(lua_State *L, const EventParams &params)
{
	lua_newtable(L);

	// Push _types sub-table
	lua_newtable(L);

	for (EventParams::ConstIterator it = params.begin(); it != params.end();
	     ++it) {
		const std::string &key = it->first;
		const EventValue &val = it->second;

		// Push the value
		pushEventValue(L, val);
		lua_setfield(L, -3, key.c_str());

		// Push the type name into _types
		lua_pushstring(L, eventValueTypeName(val.getType()));
		lua_setfield(L, -2, key.c_str());
	}

	// Set _types sub-table
	lua_setfield(L, -2, "_types");
}

// ---------------------------------------------------------------------------
// Helper: read a Lua value as an EventValue
// ---------------------------------------------------------------------------

/**
 * @brief Read a Lua value at the given index as an EventValue.
 *
 * Type inference rules:
 *   - integer -> INT
 *   - number (non-integer) -> DOUBLE
 *   - string -> STRING
 *   - table with all integer keys -> array (type inferred from elements)
 *   - table with string keys -> not supported at value level
 *
 * @param L   Lua state.
 * @param idx Stack index of the value.
 * @return    EventValue representing the Lua value.
 */
static EventValue readLuaValue(lua_State *L, int idx)
{
	int absIdx = lua_absindex(L, idx);
	int type = lua_type(L, absIdx);

	switch (type) {
	case LUA_TNIL:
		return EventValue();

	case LUA_TNUMBER: {
		lua_Number num = lua_tonumber(L, absIdx);
		lua_Integer intVal = lua_tointeger(L, absIdx);
		// Check if it's an integer (within precision)
		if ((lua_Number)intVal == num) {
			return EventValue((int64_t)intVal);
		}
		return EventValue((double)num);
	}

	case LUA_TSTRING:
		return EventValue(lua_tostring(L, absIdx));

	case LUA_TBOOLEAN:
		return EventValue((int64_t)(lua_toboolean(L, absIdx) ? 1 : 0));

	case LUA_TTABLE: {
		// Check if it's an array (all integer keys)
		bool isArray = true;
		int maxKey = 0;
		bool hasStringElements = false;
		bool hasNumberElements = false;
		bool hasIntegerElements = false;

		lua_pushnil(L);
		while (lua_next(L, absIdx) != 0) {
			if (lua_type(L, -2) == LUA_TNUMBER) {
				int k = (int)lua_tointeger(L, -2);
				if (k > maxKey)
					maxKey = k;
				int elemType = lua_type(L, -1);
				if (elemType == LUA_TSTRING)
					hasStringElements = true;
				else if (elemType == LUA_TNUMBER) {
					lua_Number num = lua_tonumber(L, -1);
					lua_Integer intVal =
						lua_tointeger(L, -1);
					if ((lua_Number)intVal == num)
						hasIntegerElements = true;
					else
						hasNumberElements = true;
				}
			} else {
				isArray = false;
			}
			lua_pop(L, 1);
		}

		if (isArray && maxKey > 0) {
			if (hasStringElements) {
				std::vector<std::string> arr;
				for (int i = 1; i <= maxKey; i++) {
					lua_rawgeti(L, absIdx, i);
					if (lua_type(L, -1) == LUA_TSTRING)
						arr.push_back(
							lua_tostring(L, -1));
					lua_pop(L, 1);
				}
				return EventValue(arr);
			} else if (hasIntegerElements) {
				std::vector<int64_t> arr;
				for (int i = 1; i <= maxKey; i++) {
					lua_rawgeti(L, absIdx, i);
					if (lua_type(L, -1) == LUA_TNUMBER)
						arr.push_back(
							(int64_t)lua_tointeger(
								L, -1));
					lua_pop(L, 1);
				}
				return EventValue(arr);
			} else if (hasNumberElements) {
				std::vector<double> arr;
				for (int i = 1; i <= maxKey; i++) {
					lua_rawgeti(L, absIdx, i);
					if (lua_type(L, -1) == LUA_TNUMBER)
						arr.push_back(
							lua_tonumber(L, -1));
					lua_pop(L, 1);
				}
				return EventValue(arr);
			}
		}

		// Not an array or empty - return nil
		return EventValue();
	}

	default:
		return EventValue();
	}
}

// ---------------------------------------------------------------------------
// Helper: read a Lua table as EventParams
// ---------------------------------------------------------------------------

/**
 * @brief Read a Lua table at the given index as EventParams.
 *
 * Each key-value pair in the table is converted to an EventValue.
 * The _types sub-table is NOT read back (types are inferred from values).
 *
 * @param L   Lua state.
 * @param idx Stack index of the table.
 * @return    EventParams populated from the table.
 */
EventParams readEventParams(lua_State *L, int idx)
{
	EventParams params;
	int absIdx = lua_absindex(L, idx);

	if (!lua_istable(L, absIdx))
		return params;

	lua_pushnil(L);
	while (lua_next(L, absIdx) != 0) {
		if (lua_type(L, -2) == LUA_TSTRING) {
			const char *key = lua_tostring(L, -2);
			if (key) {
				// Skip _types key (metadata)
				if (strcmp(key, "_types") == 0) {
					lua_pop(L, 1);
					continue;
				}
				params.m_values[key] = readLuaValue(L, -1);
			}
		}
		lua_pop(L, 1);
	}

	return params;
}

// ---------------------------------------------------------------------------
// Lua: ecs.send_event("eventName", [params_table])
// ---------------------------------------------------------------------------

/**
 * @brief Lua: ecs.send_event("eventName", [params_table]) -> nil
 *
 * Sends an event through the global EventBus. The optional params table
 * is converted to an EventParams payload.
 *
 * Usage:
 *   ecs.send_event("collision")
 *   ecs.send_event("collision", { entity_id = 42, damage = 10 })
 *   ecs.send_event("collision", { targets = {100, 101, 102} })
 */
static int luaSendEvent(lua_State *L)
{
	const char *eventName = luaL_checkstring(L, 1);

	EventParams params;
	if (lua_gettop(L) >= 2 && lua_istable(L, 2)) {
		params = readEventParams(L, 2);
	}

	EventBus::getInstance().send(eventName, params);
	return 0;
}

// ---------------------------------------------------------------------------
// Lua: ecs.subscribe_event("eventName", callback_fn) -> int (subscription id)
// ---------------------------------------------------------------------------

/**
 * @brief Lua: ecs.subscribe_event("eventName", callback_fn) -> int
 *
 * Subscribes a Lua function to an event. The callback receives:
 *   function(event_name, params_table)
 *
 * Returns a subscription ID that can be used with unsubscribe_event().
 *
 * Usage:
 *   local sub_id = ecs.subscribe_event("collision", function(event, params)
 *       print("Collision event received!")
 *       print("entity_id: " .. params.entity_id)
 *   end)
 */
static int luaSubscribeEvent(lua_State *L)
{
	const char *eventName = luaL_checkstring(L, 1);
	luaL_checktype(L, 2, LUA_TFUNCTION);

	// Create a reference to the Lua callback function
	lua_pushvalue(L, 2);
	int callbackRef = luaL_ref(L, LUA_REGISTRYINDEX);

	// Subscribe to the EventBus with a C++ lambda that calls the Lua function
	EventBus::ListenerId listenerId = EventBus::getInstance().subscribe(
		eventName, [L, callbackRef](const Ogre::String &eventName,
					    const EventParams &params) {
			// Push the Lua callback function
			lua_rawgeti(L, LUA_REGISTRYINDEX, callbackRef);

			// Push event name
			lua_pushstring(L, eventName.c_str());

			// Push params as a Lua table
			pushEventParams(L, params);

			// Call the Lua function (2 args, 0 results)
			if (lua_pcall(L, 2, 0, 0) != LUA_OK) {
				Ogre::LogManager::getSingleton().stream()
					<< "Lua event callback error: "
					<< lua_tostring(L, -1);
				lua_pop(L, 1);
			}
		});

	// Store the mapping from Lua subscription ID to EventBus listener ID
	int luaSubId = s_nextLuaSubId++;
	s_luaSubscriptions[luaSubId] = listenerId;

	lua_pushinteger(L, luaSubId);
	return 1;
}

// ---------------------------------------------------------------------------
// Lua: ecs.unsubscribe_event(subscription_id) -> nil
// ---------------------------------------------------------------------------

/**
 * @brief Lua: ecs.unsubscribe_event(subscription_id) -> nil
 *
 * Unsubscribes a previously registered event subscription.
 *
 * Usage:
 *   ecs.unsubscribe_event(sub_id)
 */
static int luaUnsubscribeEvent(lua_State *L)
{
	int luaSubId = (int)luaL_checkinteger(L, 1);

	auto it = s_luaSubscriptions.find(luaSubId);
	if (it != s_luaSubscriptions.end()) {
		EventBus::getInstance().unsubscribe(it->second);
		s_luaSubscriptions.erase(it);
	}

	return 0;
}

// ---------------------------------------------------------------------------
// Public registration function
// ---------------------------------------------------------------------------

void registerLuaEventApi(lua_State *L)
{
	// Get or create the "ecs" global table
	lua_getglobal(L, "ecs");
	if (lua_isnil(L, -1)) {
		lua_newtable(L);
		lua_setglobal(L, "ecs");
		lua_getglobal(L, "ecs");
	}

	// Register event functions
	lua_pushcfunction(L, luaSendEvent);
	lua_setfield(L, -2, "send_event");

	lua_pushcfunction(L, luaSubscribeEvent);
	lua_setfield(L, -2, "subscribe_event");

	lua_pushcfunction(L, luaUnsubscribeEvent);
	lua_setfield(L, -2, "unsubscribe_event");

	// Pop the ecs table
	lua_pop(L, 1);
}

} // namespace editScene
