#include "LuaActionApi.hpp"
#include "../components/ActionDatabase.hpp"
#include "../components/GoapBlackboard.hpp"
#include "../components/BehaviorTree.hpp"
#include <OgreLogManager.h>
#include <cstring>

namespace editScene
{

// ---------------------------------------------------------------------------
// Helper: push a BehaviorTreeNode as a Lua table
// ---------------------------------------------------------------------------

static void pushBehaviorTree(lua_State *L, const BehaviorTreeNode &node)
{
	lua_newtable(L);

	lua_pushstring(L, node.type.c_str());
	lua_setfield(L, -2, "type");

	if (!node.name.empty()) {
		lua_pushstring(L, node.name.c_str());
		lua_setfield(L, -2, "name");
	}

	if (!node.params.empty()) {
		lua_pushstring(L, node.params.c_str());
		lua_setfield(L, -2, "params");
	}

	if (!node.children.empty()) {
		lua_newtable(L);
		for (size_t i = 0; i < node.children.size(); i++) {
			pushBehaviorTree(L, node.children[i]);
			lua_rawseti(L, -2, (int)i + 1);
		}
		lua_setfield(L, -2, "children");
	}
}

// ---------------------------------------------------------------------------
// Helper: read a BehaviorTreeNode from a Lua table at given index
// ---------------------------------------------------------------------------

static BehaviorTreeNode readBehaviorTree(lua_State *L, int idx)
{
	BehaviorTreeNode node;

	if (!lua_istable(L, idx))
		return node;

	// type (required)
	lua_getfield(L, idx, "type");
	if (lua_isstring(L, -1))
		node.type = lua_tostring(L, -1);
	lua_pop(L, 1);

	// name (optional)
	lua_getfield(L, idx, "name");
	if (lua_isstring(L, -1))
		node.name = lua_tostring(L, -1);
	lua_pop(L, 1);

	// params (optional)
	lua_getfield(L, idx, "params");
	if (lua_isstring(L, -1))
		node.params = lua_tostring(L, -1);
	lua_pop(L, 1);

	// children (optional array)
	lua_getfield(L, idx, "children");
	if (lua_istable(L, -1)) {
		lua_pushnil(L);
		while (lua_next(L, -2) != 0) {
			// key is numeric index, value is child table
			if (lua_istable(L, -1)) {
				node.children.push_back(
					readBehaviorTree(L, lua_gettop(L)));
			}
			lua_pop(L, 1);
		}
	}
	lua_pop(L, 1);

	return node;
}

// ---------------------------------------------------------------------------
// Helper: push a GoapBlackboard as a Lua table
// ---------------------------------------------------------------------------

static void pushBlackboard(lua_State *L, const GoapBlackboard &bb)
{
	lua_newtable(L); // blackboard table

	// Bits
	lua_newtable(L); // bits table
	for (int i = 0; i < 64; i++) {
		if (bb.hasBit(i)) {
			const char *name = GoapBlackboard::getBitName(i);
			const char *key = name ? name : "";
			lua_pushboolean(L, bb.getBit(i));
			lua_setfield(L, -2, key);
		}
	}
	lua_setfield(L, -2, "bits");

	// Integer values
	lua_newtable(L);
	for (const auto &kv : bb.values) {
		lua_pushinteger(L, kv.second);
		lua_setfield(L, -2, kv.first.c_str());
	}
	lua_setfield(L, -2, "values");

	// Float values
	lua_newtable(L);
	for (const auto &kv : bb.floatValues) {
		lua_pushnumber(L, kv.second);
		lua_setfield(L, -2, kv.first.c_str());
	}
	lua_setfield(L, -2, "floatValues");

	// String values
	lua_newtable(L);
	for (const auto &kv : bb.stringValues) {
		lua_pushstring(L, kv.second.c_str());
		lua_setfield(L, -2, kv.first.c_str());
	}
	lua_setfield(L, -2, "stringValues");
}

// ---------------------------------------------------------------------------
// Helper: read a GoapBlackboard from a Lua table (optional, at given index)
// ---------------------------------------------------------------------------

static GoapBlackboard readBlackboard(lua_State *L, int idx)
{
	GoapBlackboard bb;

	if (!lua_istable(L, idx))
		return bb;

	// Read bits
	lua_getfield(L, idx, "bits");
	if (lua_istable(L, -1)) {
		lua_pushnil(L);
		while (lua_next(L, -2) != 0) {
			// key is the bit name (string), value is boolean
			if (lua_isstring(L, -2) && lua_isboolean(L, -1)) {
				const char *name = lua_tostring(L, -2);
				bool val = lua_toboolean(L, -1) != 0;
				// Find bit index by name (auto-registers if new)
				int idx2 = GoapBlackboard::findBitByName(name);
				if (idx2 < 0) {
					// Find first free slot
					for (int i = 0; i < 64; i++) {
						if (GoapBlackboard::getBitName(
							    i) == nullptr) {
							GoapBlackboard::setBitName(
								i, name);
							idx2 = i;
							break;
						}
					}
				}
				if (idx2 >= 0)
					bb.setBit(idx2, val);
			}
			lua_pop(L, 1);
		}
	}
	lua_pop(L, 1);

	// Read integer values
	lua_getfield(L, idx, "values");
	if (lua_istable(L, -1)) {
		lua_pushnil(L);
		while (lua_next(L, -2) != 0) {
			if (lua_isstring(L, -2) && lua_isinteger(L, -1))
				bb.values[lua_tostring(L, -2)] =
					(int)lua_tointeger(L, -1);
			lua_pop(L, 1);
		}
	}
	lua_pop(L, 1);

	// Read float values
	lua_getfield(L, idx, "floatValues");
	if (lua_istable(L, -1)) {
		lua_pushnil(L);
		while (lua_next(L, -2) != 0) {
			if (lua_isstring(L, -2) && lua_isnumber(L, -1))
				bb.floatValues[lua_tostring(L, -2)] =
					(float)lua_tonumber(L, -1);
			lua_pop(L, 1);
		}
	}
	lua_pop(L, 1);

	// Read string values
	lua_getfield(L, idx, "stringValues");
	if (lua_istable(L, -1)) {
		lua_pushnil(L);
		while (lua_next(L, -2) != 0) {
			if (lua_isstring(L, -2) && lua_isstring(L, -1))
				bb.stringValues[lua_tostring(L, -2)] =
					lua_tostring(L, -1);
			lua_pop(L, 1);
		}
	}
	lua_pop(L, 1);

	return bb;
}

// ---------------------------------------------------------------------------
// Lua: ecs.action_db.add_action(name, cost, preconds, effects, behaviorTree)
// ---------------------------------------------------------------------------

static int luaAddAction(lua_State *L)
{
	const char *name = luaL_checkstring(L, 1);
	int cost = (int)luaL_optinteger(L, 2, 1);

	GoapAction action(name, cost);

	// Optional preconditions table (arg 3)
	if (lua_gettop(L) >= 3 && lua_istable(L, 3))
		action.preconditions = readBlackboard(L, 3);

	// Optional effects table (arg 4)
	if (lua_gettop(L) >= 4 && lua_istable(L, 4))
		action.effects = readBlackboard(L, 4);

	// Optional behavior tree table (arg 5)
	if (lua_gettop(L) >= 5 && lua_istable(L, 5))
		action.behaviorTree = readBehaviorTree(L, 5);

	ActionDatabase::getSingleton().addOrReplaceAction(action);

	Ogre::LogManager::getSingleton().stream()
		<< "[Lua] Added action: " << name;
	return 0;
}

// ---------------------------------------------------------------------------
// Lua: ecs.action_db.add_goal(name, priority, target, condition)
// ---------------------------------------------------------------------------

static int luaAddGoal(lua_State *L)
{
	const char *name = luaL_checkstring(L, 1);
	int priority = (int)luaL_optinteger(L, 2, 1);

	GoapGoal goal(name, priority);

	// Optional target blackboard (arg 3)
	if (lua_gettop(L) >= 3 && lua_istable(L, 3))
		goal.target = readBlackboard(L, 3);

	// Optional condition string (arg 4)
	if (lua_gettop(L) >= 4 && lua_isstring(L, 4))
		goal.condition = lua_tostring(L, 4);

	ActionDatabase::getSingleton().addOrReplaceGoal(goal);

	Ogre::LogManager::getSingleton().stream()
		<< "[Lua] Added goal: " << name;
	return 0;
}

// ---------------------------------------------------------------------------
// Lua: ecs.action_db.remove_action(name) -> bool
// ---------------------------------------------------------------------------

static int luaRemoveAction(lua_State *L)
{
	const char *name = luaL_checkstring(L, 1);
	bool removed = ActionDatabase::getSingleton().removeAction(name);
	lua_pushboolean(L, removed);
	return 1;
}

// ---------------------------------------------------------------------------
// Lua: ecs.action_db.remove_goal(name) -> bool
// ---------------------------------------------------------------------------

static int luaRemoveGoal(lua_State *L)
{
	const char *name = luaL_checkstring(L, 1);
	bool removed = ActionDatabase::getSingleton().removeGoal(name);
	lua_pushboolean(L, removed);
	return 1;
}

// ---------------------------------------------------------------------------
// Lua: ecs.action_db.find_action(name) -> table or nil
// ---------------------------------------------------------------------------

static int luaFindAction(lua_State *L)
{
	const char *name = luaL_checkstring(L, 1);
	const GoapAction *action =
		ActionDatabase::getSingleton().findAction(name);

	if (!action) {
		lua_pushnil(L);
		return 1;
	}

	lua_newtable(L);
	lua_pushstring(L, action->name.c_str());
	lua_setfield(L, -2, "name");
	lua_pushinteger(L, action->cost);
	lua_setfield(L, -2, "cost");

	pushBlackboard(L, action->preconditions);
	lua_setfield(L, -2, "preconditions");

	pushBlackboard(L, action->effects);
	lua_setfield(L, -2, "effects");

	// Behavior tree
	pushBehaviorTree(L, action->behaviorTree);
	lua_setfield(L, -2, "behaviorTree");

	// Behavior tree name (optional reference)
	if (!action->behaviorTreeName.empty()) {
		lua_pushstring(L, action->behaviorTreeName.c_str());
		lua_setfield(L, -2, "behaviorTreeName");
	}

	return 1;
}

// ---------------------------------------------------------------------------
// Lua: ecs.action_db.find_goal(name) -> table or nil
// ---------------------------------------------------------------------------

static int luaFindGoal(lua_State *L)
{
	const char *name = luaL_checkstring(L, 1);
	const GoapGoal *goal = ActionDatabase::getSingleton().findGoal(name);

	if (!goal) {
		lua_pushnil(L);
		return 1;
	}

	lua_newtable(L);
	lua_pushstring(L, goal->name.c_str());
	lua_setfield(L, -2, "name");
	lua_pushinteger(L, goal->priority);
	lua_setfield(L, -2, "priority");

	pushBlackboard(L, goal->target);
	lua_setfield(L, -2, "target");

	lua_pushstring(L, goal->condition.c_str());
	lua_setfield(L, -2, "condition");

	return 1;
}

// ---------------------------------------------------------------------------
// Lua: ecs.action_db.list_actions() -> table of action names
// ---------------------------------------------------------------------------

static int luaListActions(lua_State *L)
{
	const auto &db = ActionDatabase::getSingleton();
	lua_newtable(L);
	int idx = 1;
	for (const auto &action : db.actions) {
		lua_pushstring(L, action.name.c_str());
		lua_rawseti(L, -2, idx++);
	}
	return 1;
}

// ---------------------------------------------------------------------------
// Lua: ecs.action_db.list_goals() -> table of goal names
// ---------------------------------------------------------------------------

static int luaListGoals(lua_State *L)
{
	const auto &db = ActionDatabase::getSingleton();
	lua_newtable(L);
	int idx = 1;
	for (const auto &goal : db.goals) {
		lua_pushstring(L, goal.name.c_str());
		lua_rawseti(L, -2, idx++);
	}
	return 1;
}

// ---------------------------------------------------------------------------
// Lua: ecs.action_db.clear() -> nil
// ---------------------------------------------------------------------------

static int luaClear(lua_State *L)
{
	(void)L;
	ActionDatabase::getSingleton().clear();
	return 0;
}

// ---------------------------------------------------------------------------
// Lua: ecs.action_db.set_bit_name(index, name) -> nil
// ---------------------------------------------------------------------------

static int luaSetBitName(lua_State *L)
{
	int index = (int)luaL_checkinteger(L, 1);
	const char *name = luaL_checkstring(L, 2);

	if (index < 0 || index >= 64)
		luaL_error(L, "bit index must be 0-63, got %d", index);

	GoapBlackboard::setBitName(index, name);
	return 0;
}

// ---------------------------------------------------------------------------
// Lua: ecs.action_db.find_bit_by_name(name) -> index or nil
// ---------------------------------------------------------------------------

static int luaFindBitByName(lua_State *L)
{
	const char *name = luaL_checkstring(L, 1);
	int index = GoapBlackboard::findBitByName(name);
	if (index < 0) {
		lua_pushnil(L);
	} else {
		lua_pushinteger(L, index);
	}
	return 1;
}

// ---------------------------------------------------------------------------
// Lua: ecs.action_db.get_bit_name(index) -> name or nil
// ---------------------------------------------------------------------------

static int luaGetBitName(lua_State *L)
{
	int index = (int)luaL_checkinteger(L, 1);
	if (index < 0 || index >= 64) {
		lua_pushnil(L);
		return 1;
	}
	const char *name = GoapBlackboard::getBitName(index);
	if (name) {
		lua_pushstring(L, name);
	} else {
		lua_pushnil(L);
	}
	return 1;
}

// ---------------------------------------------------------------------------
// Lua: ecs.action_db.list_bit_names() -> table of { index, name }
// ---------------------------------------------------------------------------

static int luaListBitNames(lua_State *L)
{
	lua_newtable(L);
	int idx = 1;
	for (int i = 0; i < 64; i++) {
		const char *name = GoapBlackboard::getBitName(i);
		if (name) {
			lua_newtable(L);
			lua_pushinteger(L, i);
			lua_setfield(L, -2, "index");
			lua_pushstring(L, name);
			lua_setfield(L, -2, "name");
			lua_rawseti(L, -2, idx++);
		}
	}
	return 1;
}

// ---------------------------------------------------------------------------
// Lua: ecs.action_db.auto_assign_bit(name) -> index
// ---------------------------------------------------------------------------
// Finds a bit by name, or auto-assigns the first free slot if not found.
// Returns the bit index, or -1 if all 64 slots are full.

static int luaAutoAssignBit(lua_State *L)
{
	const char *name = luaL_checkstring(L, 1);
	int index = GoapBlackboard::findBitByName(name);
	if (index >= 0) {
		lua_pushinteger(L, index);
		return 1;
	}
	// Find first free slot
	for (int i = 0; i < 64; i++) {
		if (GoapBlackboard::getBitName(i) == nullptr) {
			GoapBlackboard::setBitName(i, name);
			lua_pushinteger(L, i);
			return 1;
		}
	}
	lua_pushinteger(L, -1);
	return 1;
}

// ---------------------------------------------------------------------------
// Registration
// ---------------------------------------------------------------------------

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

	// Create the action_db sub-table
	lua_newtable(L);

	lua_pushcfunction(L, luaAddAction);
	lua_setfield(L, -2, "add_action");

	lua_pushcfunction(L, luaAddGoal);
	lua_setfield(L, -2, "add_goal");

	lua_pushcfunction(L, luaRemoveAction);
	lua_setfield(L, -2, "remove_action");

	lua_pushcfunction(L, luaRemoveGoal);
	lua_setfield(L, -2, "remove_goal");

	lua_pushcfunction(L, luaFindAction);
	lua_setfield(L, -2, "find_action");

	lua_pushcfunction(L, luaFindGoal);
	lua_setfield(L, -2, "find_goal");

	lua_pushcfunction(L, luaListActions);
	lua_setfield(L, -2, "list_actions");

	lua_pushcfunction(L, luaListGoals);
	lua_setfield(L, -2, "list_goals");

	lua_pushcfunction(L, luaClear);
	lua_setfield(L, -2, "clear");

	// Bit name management
	lua_pushcfunction(L, luaSetBitName);
	lua_setfield(L, -2, "set_bit_name");

	lua_pushcfunction(L, luaFindBitByName);
	lua_setfield(L, -2, "find_bit_by_name");

	lua_pushcfunction(L, luaGetBitName);
	lua_setfield(L, -2, "get_bit_name");

	lua_pushcfunction(L, luaListBitNames);
	lua_setfield(L, -2, "list_bit_names");

	lua_pushcfunction(L, luaAutoAssignBit);
	lua_setfield(L, -2, "auto_assign_bit");

	// Set action_db as a field of ecs
	lua_setfield(L, -2, "action_db");

	// Ensure ecs is global
	lua_setglobal(L, "ecs");
}

} // namespace editScene
