#include "LuaComponentApi.hpp"
#include "LuaEntityApi.hpp"
#include <OgreLogManager.h>
#include <OgreVector3.h>
#include <OgreQuaternion.h>
#include <OgreColourValue.h>
#include <flecs.h>
#include <string>
#include <unordered_map>
#include <functional>

// Include all component headers
#include "components/EntityName.hpp"
#include "components/Transform.hpp"
#include "components/Renderable.hpp"
#include "components/EditorMarker.hpp"
#include "components/PhysicsCollider.hpp"
#include "components/RigidBody.hpp"
#include "components/BuoyancyInfo.hpp"
#include "components/InWater.hpp"
#include "components/WaterPhysics.hpp"
#include "components/WaterPlane.hpp"
#include "components/Sun.hpp"
#include "components/Skybox.hpp"
#include "components/Light.hpp"
#include "components/Camera.hpp"
#include "components/Lod.hpp"
#include "components/LodSettings.hpp"
#include "components/StaticGeometry.hpp"
#include "components/StaticGeometryMember.hpp"
#include "components/ProceduralTexture.hpp"
#include "components/ProceduralMaterial.hpp"
#include "components/Primitive.hpp"
#include "components/TriangleBuffer.hpp"
#include "components/CharacterSlots.hpp"
#include "components/CharacterSpawner.hpp"
#include "components/CharacterIdentity.hpp"
#include "systems/CharacterRegistry.hpp"
#include "components/AnimationTree.hpp"
#include "components/AnimationTreeTemplate.hpp"
#include "components/Character.hpp"
#include "components/StartupMenu.hpp"
#include "components/PlayerController.hpp"
#include "components/CellGrid.hpp"
#include "components/ActionDatabase.hpp"
#include "components/ActionDebug.hpp"
#include "components/BehaviorTree.hpp"
#include "components/GoapBlackboard.hpp"
#include "components/PrefabInstance.hpp"
#include "components/NavMesh.hpp"
#include "components/SmartObject.hpp"
#include "components/Actuator.hpp"
#include "components/GoapPlanner.hpp"
#include "components/GoapRunner.hpp"
#include "components/PathFollowing.hpp"
#include "components/EventHandler.hpp"
#include "components/Item.hpp"
#include "components/Inventory.hpp"
#include "components/GeneratedPhysicsTag.hpp"
#include "components/Relationship.hpp"

namespace editScene
{

// ---------------------------------------------------------------------------
// Helper: push a Lua table from a component's fields
// ---------------------------------------------------------------------------

/**
 * @brief Push an Ogre::Vector3 as a Lua table {x, y, z}.
 */
static void pushVector3(lua_State *L, const Ogre::Vector3 &v)
{
	lua_newtable(L);
	lua_pushnumber(L, v.x);
	lua_rawseti(L, -2, 1);
	lua_pushnumber(L, v.y);
	lua_rawseti(L, -2, 2);
	lua_pushnumber(L, v.z);
	lua_rawseti(L, -2, 3);
}

/**
 * @brief Read a Lua table at the given index as an Ogre::Vector3.
 * Expects {x, y, z} or {1=x, 2=y, 3=z}.
 */
static Ogre::Vector3 readVector3(lua_State *L, int idx)
{
	Ogre::Vector3 v;
	lua_rawgeti(L, idx, 1);
	v.x = (float)lua_tonumber(L, -1);
	lua_pop(L, 1);
	lua_rawgeti(L, idx, 2);
	v.y = (float)lua_tonumber(L, -1);
	lua_pop(L, 1);
	lua_rawgeti(L, idx, 3);
	v.z = (float)lua_tonumber(L, -1);
	lua_pop(L, 1);
	return v;
}

/**
 * @brief Push an Ogre::Quaternion as a Lua table {w, x, y, z}.
 */
static void pushQuaternion(lua_State *L, const Ogre::Quaternion &q)
{
	lua_newtable(L);
	lua_pushnumber(L, q.w);
	lua_rawseti(L, -2, 1);
	lua_pushnumber(L, q.x);
	lua_rawseti(L, -2, 2);
	lua_pushnumber(L, q.y);
	lua_rawseti(L, -2, 3);
	lua_pushnumber(L, q.z);
	lua_rawseti(L, -2, 4);
}

/**
 * @brief Read a Lua table at the given index as an Ogre::Quaternion.
 * Expects {w, x, y, z}.
 */
static Ogre::Quaternion readQuaternion(lua_State *L, int idx)
{
	Ogre::Quaternion q;
	lua_rawgeti(L, idx, 1);
	q.w = (float)lua_tonumber(L, -1);
	lua_pop(L, 1);
	lua_rawgeti(L, idx, 2);
	q.x = (float)lua_tonumber(L, -1);
	lua_pop(L, 1);
	lua_rawgeti(L, idx, 3);
	q.y = (float)lua_tonumber(L, -1);
	lua_pop(L, 1);
	lua_rawgeti(L, idx, 4);
	q.z = (float)lua_tonumber(L, -1);
	lua_pop(L, 1);
	return q;
}

/**
 * @brief Push an Ogre::ColourValue as a Lua table {r, g, b, a}.
 */
static void pushColourValue(lua_State *L, const Ogre::ColourValue &c)
{
	lua_newtable(L);
	lua_pushnumber(L, c.r);
	lua_rawseti(L, -2, 1);
	lua_pushnumber(L, c.g);
	lua_rawseti(L, -2, 2);
	lua_pushnumber(L, c.b);
	lua_rawseti(L, -2, 3);
	lua_pushnumber(L, c.a);
	lua_rawseti(L, -2, 4);
}

/**
 * @brief Read a Lua table at the given index as an Ogre::ColourValue.
 * Expects {r, g, b, a}.
 */
static Ogre::ColourValue readColourValue(lua_State *L, int idx)
{
	Ogre::ColourValue c;
	lua_rawgeti(L, idx, 1);
	c.r = (float)lua_tonumber(L, -1);
	lua_pop(L, 1);
	lua_rawgeti(L, idx, 2);
	c.g = (float)lua_tonumber(L, -1);
	lua_pop(L, 1);
	lua_rawgeti(L, idx, 3);
	c.b = (float)lua_tonumber(L, -1);
	lua_pop(L, 1);
	lua_rawgeti(L, idx, 4);
	c.a = (float)lua_tonumber(L, -1);
	lua_pop(L, 1);
	return c;
}

/**
 * @brief Push a string vector as a Lua table.
 */
static void pushStringVector(lua_State *L, const std::vector<Ogre::String> &vec)
{
	lua_newtable(L);
	for (size_t i = 0; i < vec.size(); i++) {
		lua_pushstring(L, vec[i].c_str());
		lua_rawseti(L, -2, (int)(i + 1));
	}
}

/**
 * @brief Read a Lua table as a string vector.
 */
static std::vector<Ogre::String> readStringVector(lua_State *L, int idx)
{
	std::vector<Ogre::String> result;
	lua_pushnil(L);
	while (lua_next(L, idx) != 0) {
		if (lua_isstring(L, -1))
			result.push_back(lua_tostring(L, -1));
		lua_pop(L, 1);
	}
	return result;
}

// ---------------------------------------------------------------------------
// Helper: get the Flecs world from the Lua registry
// ---------------------------------------------------------------------------

static flecs::world getWorld(lua_State *L)
{
	lua_getfield(L, LUA_REGISTRYINDEX, "EditSceneFlecsWorld");
	OgreAssert(lua_islightuserdata(L, -1), "Flecs world not registered");
	flecs::world *world =
		static_cast<flecs::world *>(lua_touserdata(L, -1));
	lua_pop(L, 1);
	return *world;
}

// ---------------------------------------------------------------------------
// Component descriptor: maps component name to add/get/set/has/remove ops
// ---------------------------------------------------------------------------

struct ComponentDescriptor {
	const char *name;
	std::function<void(lua_State *, flecs::entity)> pushFields;
	std::function<void(lua_State *, flecs::entity, int)> readFields;
};

static std::unordered_map<std::string, ComponentDescriptor> s_components;

// ---------------------------------------------------------------------------
// Registration helper macro
// ---------------------------------------------------------------------------

#define REGISTER_COMPONENT(CompType, NameStr, PushBody, ReadBody)    \
	do {                                                         \
		s_components[NameStr] = {                            \
			NameStr,                                     \
			[](lua_State *L, flecs::entity e) {          \
				if (!e.has<CompType>()) {            \
					lua_pushnil(L);              \
					return;                      \
				}                                    \
				const auto &c = e.get<CompType>();   \
				lua_newtable(L);                     \
				PushBody;                            \
			},                                           \
			[](lua_State *L, flecs::entity e, int idx) { \
				CompType c;                          \
				if (e.has<CompType>())               \
					c = e.get<CompType>();       \
				ReadBody;                            \
				e.set<CompType>(c);                  \
			}                                            \
		};                                                   \
	} while (0)

// ---------------------------------------------------------------------------
// Register all components
// ---------------------------------------------------------------------------

static void registerAllComponents()
{
	if (!s_components.empty())
		return;

	// --- EntityName ---
	REGISTER_COMPONENT(EntityNameComponent, "EntityName",
			   lua_pushstring(L, c.name.c_str());
			   lua_setfield(L, -2, "name");
			   , if (lua_getfield(L, idx, "name"),
				 lua_isstring(L, -1))
				     c.name = lua_tostring(L, -1);
			   lua_pop(L, 1););

	// --- Transform ---
	REGISTER_COMPONENT(
		TransformComponent, "Transform", pushVector3(L, c.position);
		lua_setfield(L, -2, "position"); pushQuaternion(L, c.rotation);
		lua_setfield(L, -2, "rotation"); pushVector3(L, c.scale);
		lua_setfield(L, -2, "scale");
		, if (lua_getfield(L, idx, "position"), lua_istable(L, -1))
			  c.position = readVector3(L, lua_gettop(L));
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "rotation"), lua_istable(L, -1))
			c.rotation = readQuaternion(L, lua_gettop(L));
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "scale"), lua_istable(L, -1))
			c.scale = readVector3(L, lua_gettop(L));
		lua_pop(L, 1););

	// --- Renderable ---
	REGISTER_COMPONENT(
		RenderableComponent, "Renderable",
		lua_pushstring(L, c.meshName.c_str());
		lua_setfield(L, -2, "meshName");
		lua_pushboolean(L, c.visible ? 1 : 0);
		lua_setfield(L, -2, "visible");
		, if (lua_getfield(L, idx, "meshName"), lua_isstring(L, -1))
			  c.meshName = lua_tostring(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "visible"), lua_isboolean(L, -1))
			c.visible = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1););

	// --- EditorMarker (tag, no fields) ---
	s_components["EditorMarker"] = {
		"EditorMarker",
		[](lua_State *L, flecs::entity e) {
			lua_pushboolean(L,
					e.has<EditorMarkerComponent>() ? 1 : 0);
		},
		[](lua_State *L, flecs::entity e, int idx) {
			(void)idx;
			if (lua_toboolean(L, -1))
				e.add<EditorMarkerComponent>();
			else
				e.remove<EditorMarkerComponent>();
		}
	};

	// --- Light ---
	REGISTER_COMPONENT(
		LightComponent, "Light", lua_pushinteger(L, (int)c.lightType);
		lua_setfield(L, -2, "lightType");
		pushColourValue(L, c.diffuseColor);
		lua_setfield(L, -2, "diffuseColor");
		pushColourValue(L, c.specularColor);
		lua_setfield(L, -2, "specularColor");
		lua_pushnumber(L, c.intensity);
		lua_setfield(L, -2, "intensity"); lua_pushnumber(L, c.range);
		lua_setfield(L, -2, "range");
		lua_pushnumber(L, c.spotlightInnerAngle);
		lua_setfield(L, -2, "spotlightInnerAngle");
		lua_pushnumber(L, c.spotlightOuterAngle);
		lua_setfield(L, -2, "spotlightOuterAngle");
		pushVector3(L, c.direction); lua_setfield(L, -2, "direction");
		lua_pushboolean(L, c.castShadows ? 1 : 0);
		lua_setfield(L, -2, "castShadows");
		, if (lua_getfield(L, idx, "lightType"), lua_isnumber(L, -1))
			  c.lightType =
			  (LightComponent::LightType)lua_tointeger(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "diffuseColor"), lua_istable(L, -1))
			c.diffuseColor = readColourValue(L, lua_gettop(L));
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "specularColor"), lua_istable(L, -1))
			c.specularColor = readColourValue(L, lua_gettop(L));
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "intensity"), lua_isnumber(L, -1))
			c.intensity = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "range"), lua_isnumber(L, -1))
			c.range = (float)lua_tonumber(L, -1);
		lua_pop(L, 1); if (lua_getfield(L, idx, "spotlightInnerAngle"),
				   lua_isnumber(L, -1)) c.spotlightInnerAngle =
				       (float)lua_tonumber(L, -1);
		lua_pop(L, 1); if (lua_getfield(L, idx, "spotlightOuterAngle"),
				   lua_isnumber(L, -1)) c.spotlightOuterAngle =
				       (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "direction"), lua_istable(L, -1))
			c.direction = readVector3(L, lua_gettop(L));
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "castShadows"), lua_isboolean(L, -1))
			c.castShadows = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1););

	// --- Camera ---
	REGISTER_COMPONENT(
		CameraComponent, "Camera", lua_pushnumber(L, c.fovY);
		lua_setfield(L, -2, "fovY"); lua_pushnumber(L, c.nearClip);
		lua_setfield(L, -2, "nearClip"); lua_pushnumber(L, c.farClip);
		lua_setfield(L, -2, "farClip");
		lua_pushboolean(L, c.orthographic ? 1 : 0);
		lua_setfield(L, -2, "orthographic");
		lua_pushnumber(L, c.orthoWidth);
		lua_setfield(L, -2, "orthoWidth");
		lua_pushnumber(L, c.orthoHeight);
		lua_setfield(L, -2, "orthoHeight");
		, if (lua_getfield(L, idx, "fovY"), lua_isnumber(L, -1))
			  c.fovY = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "nearClip"), lua_isnumber(L, -1))
			c.nearClip = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "farClip"), lua_isnumber(L, -1))
			c.farClip = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "orthographic"), lua_isboolean(L, -1))
			c.orthographic = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "orthoWidth"), lua_isnumber(L, -1))
			c.orthoWidth = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "orthoHeight"), lua_isnumber(L, -1))
			c.orthoHeight = (float)lua_tonumber(L, -1);
		lua_pop(L, 1););

	// --- RigidBody ---
	REGISTER_COMPONENT(
		RigidBodyComponent, "RigidBody",
		lua_pushinteger(L, (int)c.bodyType);
		lua_setfield(L, -2, "bodyType"); lua_pushnumber(L, c.mass);
		lua_setfield(L, -2, "mass"); lua_pushnumber(L, c.friction);
		lua_setfield(L, -2, "friction");
		lua_pushnumber(L, c.restitution);
		lua_setfield(L, -2, "restitution");
		lua_pushboolean(L, c.isSensor ? 1 : 0);
		lua_setfield(L, -2, "isSensor");
		lua_pushboolean(L, c.enabled ? 1 : 0);
		lua_setfield(L, -2, "enabled");
		, if (lua_getfield(L, idx, "bodyType"), lua_isnumber(L, -1))
			  c.bodyType =
			  (RigidBodyComponent::BodyType)lua_tointeger(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "mass"), lua_isnumber(L, -1))
			c.mass = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "friction"), lua_isnumber(L, -1))
			c.friction = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "restitution"), lua_isnumber(L, -1))
			c.restitution = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "isSensor"), lua_isboolean(L, -1))
			c.isSensor = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "enabled"), lua_isboolean(L, -1))
			c.enabled = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1););

	// --- PhysicsCollider ---
	REGISTER_COMPONENT(
		PhysicsColliderComponent, "PhysicsCollider",
		lua_pushinteger(L, (int)c.shapeType);
		lua_setfield(L, -2, "shapeType"); pushVector3(L, c.parameters);
		lua_setfield(L, -2, "parameters"); lua_pushnumber(L, c.radius);
		lua_setfield(L, -2, "radius"); lua_pushnumber(L, c.halfHeight);
		lua_setfield(L, -2, "halfHeight");
		lua_pushstring(L, c.meshName.c_str());
		lua_setfield(L, -2, "meshName"); pushVector3(L, c.offset);
		lua_setfield(L, -2, "offset");
		, if (lua_getfield(L, idx, "shapeType"), lua_isnumber(L, -1))
			  c.shapeType = (PhysicsColliderComponent::ShapeType)
			  lua_tointeger(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "parameters"), lua_istable(L, -1))
			c.parameters = readVector3(L, lua_gettop(L));
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "radius"), lua_isnumber(L, -1))
			c.radius = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "halfHeight"), lua_isnumber(L, -1))
			c.halfHeight = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "meshName"), lua_isstring(L, -1))
			c.meshName = lua_tostring(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "offset"), lua_istable(L, -1))
			c.offset = readVector3(L, lua_gettop(L));
		lua_pop(L, 1););

	// --- Character ---
	REGISTER_COMPONENT(
		CharacterComponent, "Character", lua_pushnumber(L, c.radius);
		lua_setfield(L, -2, "radius"); lua_pushnumber(L, c.height);
		lua_setfield(L, -2, "height"); pushVector3(L, c.offset);
		lua_setfield(L, -2, "offset"); pushVector3(L, c.linearVelocity);
		lua_setfield(L, -2, "linearVelocity");
		lua_pushboolean(L, c.enabled ? 1 : 0);
		lua_setfield(L, -2, "enabled");
		lua_pushnumber(L, c.headRadius);
		lua_setfield(L, -2, "headRadius");
		lua_pushnumber(L, c.headOffsetY);
		lua_setfield(L, -2, "headOffsetY");
		lua_pushstring(L, c.headBoneName.c_str());
		lua_setfield(L, -2, "headBoneName");
		pushVector3(L, c.chestHalfExtents);
		lua_setfield(L, -2, "chestHalfExtents");
		lua_pushnumber(L, c.chestOffsetY);
		lua_setfield(L, -2, "chestOffsetY");
		lua_pushstring(L, c.chestBoneName.c_str());
		lua_setfield(L, -2, "chestBoneName");
		lua_pushboolean(L, c.useGravity ? 1 : 0);
		lua_setfield(L, -2, "useGravity");
		, if (lua_getfield(L, idx, "radius"), lua_isnumber(L, -1))
			  c.radius = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "height"), lua_isnumber(L, -1))
			c.height = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "offset"), lua_istable(L, -1))
			c.offset = readVector3(L, lua_gettop(L));
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "linearVelocity"), lua_istable(L, -1))
			c.linearVelocity = readVector3(L, lua_gettop(L));
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "enabled"), lua_isboolean(L, -1))
			c.enabled = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "headRadius"), lua_isnumber(L, -1))
			c.headRadius = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "headOffsetY"), lua_isnumber(L, -1))
			c.headOffsetY = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "headBoneName"), lua_isstring(L, -1))
			c.headBoneName = lua_tostring(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "chestHalfExtents"), lua_istable(L, -1))
			c.chestHalfExtents = readVector3(L, lua_gettop(L));
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "chestOffsetY"), lua_isnumber(L, -1))
			c.chestOffsetY = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "chestBoneName"), lua_isstring(L, -1))
			c.chestBoneName = lua_tostring(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "useGravity"), lua_isboolean(L, -1))
			c.useGravity = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1););

	// --- CharacterShapeKeys ---
	REGISTER_COMPONENT(
		CharacterShapeKeysComponent, "CharacterShapeKeys",
		{
			const CharacterRegistry::CharacterRecord *rec = nullptr;
			if (e.has<CharacterIdentityComponent>()) {
				auto &id = e.get<CharacterIdentityComponent>();
				rec = CharacterRegistry::getSingleton()
					      .findCharacter(id.registryId);
			}
			lua_newtable(L);
			if (rec) {
				for (auto &kv : rec->inlineShapeKeyWeights) {
					lua_pushnumber(L, kv.second);
					lua_setfield(L, -2, kv.first.c_str());
				}
			}
		},
		{
			if (!e.has<CharacterIdentityComponent>())
				return;
			auto &id = e.get<CharacterIdentityComponent>();
			CharacterRegistry::CharacterRecord *rec =
				CharacterRegistry::getSingleton()
					.findCharacter(id.registryId);
			if (!rec)
				return;
			if (lua_istable(L, idx)) {
				bool changed = false;
				lua_pushnil(L);
				while (lua_next(L, idx) != 0) {
					if (lua_isstring(L, -2) &&
					    lua_isnumber(L, -1)) {
						rec->inlineShapeKeyWeights[
							lua_tostring(L, -2)] =
							lua_tonumber(L, -1);
						changed = true;
					}
					lua_pop(L, 1);
				}
				if (changed) {
					CharacterRegistry::getSingleton()
						.autoSave();
					CharacterRegistry::getSingleton()
						.markCharacterDirty(
							id.registryId);
				}
			}
		});

	// --- CharacterSpawner ---
	REGISTER_COMPONENT(
		CharacterSpawnerComponent, "CharacterSpawner",
		lua_pushinteger(L, (lua_Integer)c.registryId);
		lua_setfield(L, -2, "registryId");
		lua_pushnumber(L, c.spawnDistanceSq);
		lua_setfield(L, -2, "spawnDistanceSq");
		lua_pushnumber(L, c.despawnDistanceSq);
		lua_setfield(L, -2, "despawnDistanceSq");
		, if (lua_getfield(L, idx, "registryId"), lua_isnumber(L, -1))
			c.registryId = (uint64_t)lua_tointeger(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "spawnDistanceSq"),
		    lua_isnumber(L, -1))
			c.spawnDistanceSq = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "despawnDistanceSq"),
		    lua_isnumber(L, -1))
			c.despawnDistanceSq = (float)lua_tonumber(L, -1);
		lua_pop(L, 1););

	// --- AnimationTree ---
	REGISTER_COMPONENT(
		AnimationTreeComponent, "AnimationTree",
		lua_pushboolean(L, c.enabled ? 1 : 0);
		lua_setfield(L, -2, "enabled");
		lua_pushboolean(L, c.useRootMotion ? 1 : 0);
		lua_setfield(L, -2, "useRootMotion");
		lua_pushstring(L, c.templateName.c_str());
		lua_setfield(L, -2, "templateName");
		, if (lua_getfield(L, idx, "enabled"), lua_isboolean(L, -1))
			  c.enabled = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "useRootMotion"), lua_isboolean(L, -1))
			c.useRootMotion = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "templateName"), lua_isstring(L, -1))
			c.templateName = lua_tostring(L, -1);
		lua_pop(L, 1););

	// --- AnimationTreeTemplate ---
	REGISTER_COMPONENT(AnimationTreeTemplate, "AnimationTreeTemplate",
			   lua_pushstring(L, c.name.c_str());
			   lua_setfield(L, -2, "name");
			   , if (lua_getfield(L, idx, "name"),
				 lua_isstring(L, -1))
				     c.name = lua_tostring(L, -1);
			   lua_pop(L, 1););

	// --- BehaviorTree ---
	REGISTER_COMPONENT(
		BehaviorTreeComponent, "BehaviorTree",
		lua_pushstring(L, c.treeName.c_str());
		lua_setfield(L, -2, "treeName");
		lua_pushboolean(L, c.enabled ? 1 : 0);
		lua_setfield(L, -2, "enabled");
		, if (lua_getfield(L, idx, "treeName"), lua_isstring(L, -1))
			  c.treeName = lua_tostring(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "enabled"), lua_isboolean(L, -1))
			c.enabled = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1););

	// --- GoapBlackboard ---
	REGISTER_COMPONENT(
		GoapBlackboard, "GoapBlackboard",
		lua_pushinteger(L, (lua_Integer)c.bits);
		lua_setfield(L, -2, "bits");
		lua_pushinteger(L, (lua_Integer)c.mask);
		lua_setfield(L, -2, "mask");
		// values map
		lua_newtable(L); for (auto &kv : c.values) {
			lua_pushinteger(L, kv.second);
			lua_setfield(L, -2, kv.first.c_str());
		} lua_setfield(L, -2, "values");
		// floatValues map
		lua_newtable(L); for (auto &kv : c.floatValues) {
			lua_pushnumber(L, kv.second);
			lua_setfield(L, -2, kv.first.c_str());
		} lua_setfield(L, -2, "floatValues");
		// stringValues map
		lua_newtable(L); for (auto &kv : c.stringValues) {
			lua_pushstring(L, kv.second.c_str());
			lua_setfield(L, -2, kv.first.c_str());
		} lua_setfield(L, -2, "stringValues");
		, if (lua_getfield(L, idx, "bits"), lua_isnumber(L, -1))
			  c.bits = (uint64_t)lua_tointeger(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "mask"), lua_isnumber(L, -1))
			c.mask = (uint64_t)lua_tointeger(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "values"), lua_istable(L, -1)) {
			c.values.clear();
			lua_pushnil(L);
			while (lua_next(L, -2) != 0) {
				if (lua_isstring(L, -2) && lua_isnumber(L, -1))
					c.values[lua_tostring(L, -2)] =
						(int)lua_tointeger(L, -1);
				lua_pop(L, 1);
			}
		} lua_pop(L, 1);
		if (lua_getfield(L, idx, "floatValues"), lua_istable(L, -1)) {
			c.floatValues.clear();
			lua_pushnil(L);
			while (lua_next(L, -2) != 0) {
				if (lua_isstring(L, -2) && lua_isnumber(L, -1))
					c.floatValues[lua_tostring(L, -2)] =
						(float)lua_tonumber(L, -1);
				lua_pop(L, 1);
			}
		} lua_pop(L, 1);
		if (lua_getfield(L, idx, "stringValues"), lua_istable(L, -1)) {
			c.stringValues.clear();
			lua_pushnil(L);
			while (lua_next(L, -2) != 0) {
				if (lua_isstring(L, -2) && lua_isstring(L, -1))
					c.stringValues[lua_tostring(L, -2)] =
						lua_tostring(L, -1);
				lua_pop(L, 1);
			}
		} lua_pop(L, 1););

	// --- SmartObject ---
	REGISTER_COMPONENT(
		SmartObjectComponent, "SmartObject",
		lua_pushnumber(L, c.radius);
		lua_setfield(L, -2, "radius"); lua_pushnumber(L, c.height);
		lua_setfield(L, -2, "height");
		pushStringVector(L, c.actionNames);
		lua_setfield(L, -2, "actionNames");
		, if (lua_getfield(L, idx, "radius"), lua_isnumber(L, -1))
			  c.radius = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "height"), lua_isnumber(L, -1))
			c.height = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "actionNames"), lua_istable(L, -1))
			c.actionNames = readStringVector(L, lua_gettop(L));
		lua_pop(L, 1););

	// --- Actuator ---
	REGISTER_COMPONENT(
		ActuatorComponent, "Actuator", lua_pushnumber(L, c.radius);
		lua_setfield(L, -2, "radius"); lua_pushnumber(L, c.height);
		lua_setfield(L, -2, "height");
		pushStringVector(L, c.actionNames);
		lua_setfield(L, -2, "actionNames");
		, if (lua_getfield(L, idx, "radius"), lua_isnumber(L, -1))
			  c.radius = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "height"), lua_isnumber(L, -1))
			c.height = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "actionNames"), lua_istable(L, -1))
			c.actionNames = readStringVector(L, lua_gettop(L));
		lua_pop(L, 1););

	// --- EventHandler ---
	REGISTER_COMPONENT(
		EventHandlerComponent, "EventHandler",
		lua_pushstring(L, c.eventName.c_str());
		lua_setfield(L, -2, "eventName");
		lua_pushstring(L, c.actionName.c_str());
		lua_setfield(L, -2, "actionName");
		lua_pushboolean(L, c.enabled ? 1 : 0);
		lua_setfield(L, -2, "enabled");
		, if (lua_getfield(L, idx, "eventName"), lua_isstring(L, -1))
			  c.eventName = lua_tostring(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "actionName"), lua_isstring(L, -1))
			c.actionName = lua_tostring(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "enabled"), lua_isboolean(L, -1))
			c.enabled = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1););

	// --- PathFollowing ---
	REGISTER_COMPONENT(
		PathFollowingComponent, "PathFollowing",
		lua_pushnumber(L, c.walkSpeed);
		lua_setfield(L, -2, "walkSpeed"); lua_pushnumber(L, c.runSpeed);
		lua_setfield(L, -2, "runSpeed");
		lua_pushboolean(L, c.useRootMotion ? 1 : 0);
		lua_setfield(L, -2, "useRootMotion");
		lua_pushstring(L, c.currentLocomotionState.c_str());
		lua_setfield(L, -2, "currentLocomotionState");
		, if (lua_getfield(L, idx, "walkSpeed"), lua_isnumber(L, -1))
			  c.walkSpeed = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "runSpeed"), lua_isnumber(L, -1))
			c.runSpeed = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "useRootMotion"), lua_isboolean(L, -1))
			c.useRootMotion = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "currentLocomotionState"),
		    lua_isstring(L, -1))
			c.currentLocomotionState = lua_tostring(L, -1);
		lua_pop(L, 1););

	// --- NavMesh ---
	REGISTER_COMPONENT(
		NavMeshComponent, "NavMesh", lua_pushnumber(L, c.cellSize);
		lua_setfield(L, -2, "cellSize");
		lua_pushnumber(L, c.cellHeight);
		lua_setfield(L, -2, "cellHeight");
		lua_pushnumber(L, c.agentHeight);
		lua_setfield(L, -2, "agentHeight");
		lua_pushnumber(L, c.agentRadius);
		lua_setfield(L, -2, "agentRadius");
		lua_pushnumber(L, c.agentMaxClimb);
		lua_setfield(L, -2, "agentMaxClimb");
		lua_pushnumber(L, c.agentMaxSlope);
		lua_setfield(L, -2, "agentMaxSlope");
		lua_pushboolean(L, c.enabled ? 1 : 0);
		lua_setfield(L, -2, "enabled");
		lua_pushboolean(L, c.debugDraw ? 1 : 0);
		lua_setfield(L, -2, "debugDraw");
		, if (lua_getfield(L, idx, "cellSize"), lua_isnumber(L, -1))
			  c.cellSize = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "cellHeight"), lua_isnumber(L, -1))
			c.cellHeight = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "agentHeight"), lua_isnumber(L, -1))
			c.agentHeight = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "agentRadius"), lua_isnumber(L, -1))
			c.agentRadius = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "agentMaxClimb"), lua_isnumber(L, -1))
			c.agentMaxClimb = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "agentMaxSlope"), lua_isnumber(L, -1))
			c.agentMaxSlope = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "enabled"), lua_isboolean(L, -1))
			c.enabled = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "debugDraw"), lua_isboolean(L, -1))
			c.debugDraw = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1););

	// --- NavMeshAgent ---
	REGISTER_COMPONENT(
		NavMeshAgent, "NavMeshAgent", pushVector3(L, c.targetPos);
		lua_setfield(L, -2, "targetPos");
		lua_pushboolean(L, c.hasTarget ? 1 : 0);
		lua_setfield(L, -2, "hasTarget");
		, if (lua_getfield(L, idx, "targetPos"), lua_istable(L, -1))
			  c.targetPos = readVector3(L, lua_gettop(L));
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "hasTarget"), lua_isboolean(L, -1))
			c.hasTarget = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1););

	// --- NavMeshGeometrySource ---
	s_components["NavMeshGeometrySource"] = {
		"NavMeshGeometrySource",
		[](lua_State *L, flecs::entity e) {
			lua_pushboolean(L,
					e.has<NavMeshGeometrySource>() ? 1 : 0);
		},
		[](lua_State *L, flecs::entity e, int idx) {
			(void)idx;
			if (lua_toboolean(L, -1))
				e.add<NavMeshGeometrySource>();
			else
				e.remove<NavMeshGeometrySource>();
		}
	};

	// --- Item ---
	REGISTER_COMPONENT(
		ItemComponent, "Item", lua_pushstring(L, c.itemId.c_str());
		lua_setfield(L, -2, "itemId"); lua_pushinteger(L, c.stackSize);
		lua_setfield(L, -2, "stackSize");
		lua_pushstring(L, c.action.c_str());
		lua_setfield(L, -2, "action");
		lua_pushstring(L, c.instanceId.c_str());
		lua_setfield(L, -2, "instanceId");
		lua_pushboolean(L, c.disabled ? 1 : 0);
		lua_setfield(L, -2, "disabled");
		, if (lua_getfield(L, idx, "itemId"), lua_isstring(L, -1))
			  c.itemId = lua_tostring(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "stackSize"), lua_isnumber(L, -1))
			c.stackSize = (int)lua_tointeger(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "action"), lua_isstring(L, -1))
			c.action = lua_tostring(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "instanceId"), lua_isstring(L, -1))
			c.instanceId = lua_tostring(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "disabled"), lua_isboolean(L, -1))
			c.disabled = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1););

	// --- Inventory ---
	REGISTER_COMPONENT(
		InventoryComponent, "Inventory", lua_pushinteger(L, c.maxSlots);
		lua_setfield(L, -2, "maxSlots"); lua_pushnumber(L, c.maxWeight);
		lua_setfield(L, -2, "maxWeight");
		lua_pushboolean(L, c.isContainer ? 1 : 0);
		lua_setfield(L, -2, "isContainer");
		lua_pushstring(L, c.containerId.c_str());
		lua_setfield(L, -2, "containerId");
		, if (lua_getfield(L, idx, "maxSlots"), lua_isnumber(L, -1))
			  c.maxSlots = (int)lua_tointeger(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "maxWeight"), lua_isnumber(L, -1))
			c.maxWeight = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "isContainer"), lua_isboolean(L, -1))
			c.isContainer = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "containerId"), lua_isstring(L, -1))
			c.containerId = lua_tostring(L, -1);
		lua_pop(L, 1););

	// --- Lod ---
	REGISTER_COMPONENT(
		LodComponent, "Lod", lua_pushstring(L, c.settingsId.c_str());
		lua_setfield(L, -2, "settingsId");
		lua_pushnumber(L, c.distanceMultiplier);
		lua_setfield(L, -2, "distanceMultiplier");
		, if (lua_getfield(L, idx, "settingsId"), lua_isstring(L, -1))
			  c.settingsId = lua_tostring(L, -1);
		lua_pop(L, 1); if (lua_getfield(L, idx, "distanceMultiplier"),
				   lua_isnumber(L, -1)) c.distanceMultiplier =
				       (float)lua_tonumber(L, -1);
		lua_pop(L, 1););

	// --- LodSettings ---
	REGISTER_COMPONENT(
		LodSettingsComponent, "LodSettings",
		lua_pushstring(L, c.settingsId.c_str());
		lua_setfield(L, -2, "settingsId");
		lua_pushinteger(L, (int)c.strategy);
		lua_setfield(L, -2, "strategy");
		lua_pushboolean(L, c.useCompression ? 1 : 0);
		lua_setfield(L, -2, "useCompression");
		lua_pushboolean(L, c.useVertexNormals ? 1 : 0);
		lua_setfield(L, -2, "useVertexNormals");
		, if (lua_getfield(L, idx, "settingsId"), lua_isstring(L, -1))
			  c.settingsId = lua_tostring(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "strategy"), lua_isnumber(L, -1))
			c.strategy =
			(LodSettingsComponent::Strategy)lua_tointeger(L, -1);
		lua_pop(L, 1); if (lua_getfield(L, idx, "useCompression"),
				   lua_isboolean(L, -1)) c.useCompression =
				       lua_toboolean(L, -1) != 0;
		lua_pop(L, 1); if (lua_getfield(L, idx, "useVertexNormals"),
				   lua_isboolean(L, -1)) c.useVertexNormals =
				       lua_toboolean(L, -1) != 0;
		lua_pop(L, 1););

	// --- StaticGeometry ---
	REGISTER_COMPONENT(
		StaticGeometryComponent, "StaticGeometry",
		lua_pushstring(L, c.regionId.c_str());
		lua_setfield(L, -2, "regionId");
		lua_pushstring(L, c.regionName.c_str());
		lua_setfield(L, -2, "regionName");
		lua_pushnumber(L, c.renderingDistance);
		lua_setfield(L, -2, "renderingDistance");
		lua_pushboolean(L, c.castShadows ? 1 : 0);
		lua_setfield(L, -2, "castShadows");
		lua_pushnumber(L, c.regionDimensions);
		lua_setfield(L, -2, "regionDimensions");
		, if (lua_getfield(L, idx, "regionId"), lua_isstring(L, -1))
			  c.regionId = lua_tostring(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "regionName"), lua_isstring(L, -1))
			c.regionName = lua_tostring(L, -1);
		lua_pop(L, 1); if (lua_getfield(L, idx, "renderingDistance"),
				   lua_isnumber(L, -1)) c.renderingDistance =
				       (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "castShadows"), lua_isboolean(L, -1))
			c.castShadows = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1); if (lua_getfield(L, idx, "regionDimensions"),
				   lua_isnumber(L, -1)) c.regionDimensions =
				       (float)lua_tonumber(L, -1);
		lua_pop(L, 1););

	// --- StaticGeometryMember ---
	REGISTER_COMPONENT(
		StaticGeometryMemberComponent, "StaticGeometryMember",
		lua_pushstring(L, c.meshName.c_str());
		lua_setfield(L, -2, "meshName");
		lua_pushstring(L, c.materialName.c_str());
		lua_setfield(L, -2, "materialName");
		lua_pushstring(L, c.regionId.c_str());
		lua_setfield(L, -2, "regionId");
		lua_pushboolean(L, c.castShadows ? 1 : 0);
		lua_setfield(L, -2, "castShadows");
		lua_pushboolean(L, c.visible ? 1 : 0);
		lua_setfield(L, -2, "visible");
		, if (lua_getfield(L, idx, "meshName"), lua_isstring(L, -1))
			  c.meshName = lua_tostring(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "materialName"), lua_isstring(L, -1))
			c.materialName = lua_tostring(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "regionId"), lua_isstring(L, -1))
			c.regionId = lua_tostring(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "castShadows"), lua_isboolean(L, -1))
			c.castShadows = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "visible"), lua_isboolean(L, -1))
			c.visible = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1););

	// --- ProceduralTexture ---
	REGISTER_COMPONENT(
		ProceduralTextureComponent, "ProceduralTexture",
		lua_pushstring(L, c.textureId.c_str());
		lua_setfield(L, -2, "textureId");
		lua_pushstring(L, c.textureName.c_str());
		lua_setfield(L, -2, "textureName");
		lua_pushinteger(L, c.textureSize);
		lua_setfield(L, -2, "textureSize");
		lua_pushnumber(L, c.uvMargin); lua_setfield(L, -2, "uvMargin");
		, if (lua_getfield(L, idx, "textureId"), lua_isstring(L, -1))
			  c.textureId = lua_tostring(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "textureName"), lua_isstring(L, -1))
			c.textureName = lua_tostring(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "textureSize"), lua_isnumber(L, -1))
			c.textureSize = (int)lua_tointeger(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "uvMargin"), lua_isnumber(L, -1))
			c.uvMargin = (float)lua_tonumber(L, -1);
		lua_pop(L, 1););

	// --- ProceduralMaterial ---
	REGISTER_COMPONENT(
		ProceduralMaterialComponent, "ProceduralMaterial",
		lua_pushstring(L, c.materialId.c_str());
		lua_setfield(L, -2, "materialId");
		lua_pushstring(L, c.materialName.c_str());
		lua_setfield(L, -2, "materialName");
		lua_pushstring(L, c.diffuseTextureId.c_str());
		lua_setfield(L, -2, "diffuseTextureId");
		lua_pushnumber(L, c.shininess);
		lua_setfield(L, -2, "shininess");
		lua_pushnumber(L, c.roughness);
		lua_setfield(L, -2, "roughness");
		, if (lua_getfield(L, idx, "materialId"), lua_isstring(L, -1))
			  c.materialId = lua_tostring(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "materialName"), lua_isstring(L, -1))
			c.materialName = lua_tostring(L, -1);
		lua_pop(L, 1); if (lua_getfield(L, idx, "diffuseTextureId"),
				   lua_isstring(L, -1))
				       c.diffuseTextureId = lua_tostring(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "shininess"), lua_isnumber(L, -1))
			c.shininess = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "roughness"), lua_isnumber(L, -1))
			c.roughness = (float)lua_tonumber(L, -1);
		lua_pop(L, 1););

	// --- Primitive ---
	REGISTER_COMPONENT(
		PrimitiveComponent, "Primitive",
		lua_pushinteger(L, (int)c.type);
		lua_setfield(L, -2, "type"); lua_pushnumber(L, c.boxSizeX);
		lua_setfield(L, -2, "boxSizeX"); lua_pushnumber(L, c.boxSizeY);
		lua_setfield(L, -2, "boxSizeY"); lua_pushnumber(L, c.boxSizeZ);
		lua_setfield(L, -2, "boxSizeZ");
		lua_pushnumber(L, c.planeSizeX);
		lua_setfield(L, -2, "planeSizeX");
		lua_pushnumber(L, c.planeSizeY);
		lua_setfield(L, -2, "planeSizeY");
		,
		if (lua_getfield(L, idx, "type"), lua_isnumber(L, -1))
			c.type = (PrimitiveComponent::Type)lua_tointeger(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "boxSizeX"), lua_isnumber(L, -1))
			c.boxSizeX = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "boxSizeY"), lua_isnumber(L, -1))
			c.boxSizeY = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "boxSizeZ"), lua_isnumber(L, -1))
			c.boxSizeZ = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "planeSizeX"), lua_isnumber(L, -1))
			c.planeSizeX = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "planeSizeY"), lua_isnumber(L, -1))
			c.planeSizeY = (float)lua_tonumber(L, -1);
		lua_pop(L, 1););

	// --- TriangleBuffer ---
	REGISTER_COMPONENT(
		TriangleBufferComponent, "TriangleBuffer",
		lua_pushstring(L, c.meshName.c_str());
		lua_setfield(L, -2, "meshName");
		lua_pushstring(L, c.textureRectName.c_str());
		lua_setfield(L, -2, "textureRectName");
		lua_pushboolean(L, c.useStaticGeometry ? 1 : 0);
		lua_setfield(L, -2, "useStaticGeometry");
		, if (lua_getfield(L, idx, "meshName"), lua_isstring(L, -1))
			  c.meshName = lua_tostring(L, -1);
		lua_pop(L, 1); if (lua_getfield(L, idx, "textureRectName"),
				   lua_isstring(L, -1))
				       c.textureRectName = lua_tostring(L, -1);
		lua_pop(L, 1); if (lua_getfield(L, idx, "useStaticGeometry"),
				   lua_isboolean(L, -1)) c.useStaticGeometry =
				       lua_toboolean(L, -1) != 0;
		lua_pop(L, 1););

	// --- Sun ---
	REGISTER_COMPONENT(
		SunComponent, "Sun", lua_pushboolean(L, c.enabled ? 1 : 0);
		lua_setfield(L, -2, "enabled"); lua_pushnumber(L, c.timeOfDay);
		lua_setfield(L, -2, "timeOfDay");
		lua_pushnumber(L, c.timeSpeed);
		lua_setfield(L, -2, "timeSpeed");
		pushColourValue(L, c.sunColor); lua_setfield(L, -2, "sunColor");
		pushColourValue(L, c.moonColor);
		lua_setfield(L, -2, "moonColor");
		lua_pushnumber(L, c.intensity);
		lua_setfield(L, -2, "intensity");
		lua_pushboolean(L, c.castShadows ? 1 : 0);
		lua_setfield(L, -2, "castShadows");
		, if (lua_getfield(L, idx, "enabled"), lua_isboolean(L, -1))
			  c.enabled = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "timeOfDay"), lua_isnumber(L, -1))
			c.timeOfDay = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "timeSpeed"), lua_isnumber(L, -1))
			c.timeSpeed = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "sunColor"), lua_istable(L, -1))
			c.sunColor = readColourValue(L, lua_gettop(L));
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "moonColor"), lua_istable(L, -1))
			c.moonColor = readColourValue(L, lua_gettop(L));
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "intensity"), lua_isnumber(L, -1))
			c.intensity = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "castShadows"), lua_isboolean(L, -1))
			c.castShadows = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1););

	// --- Skybox ---
	REGISTER_COMPONENT(
		SkyboxComponent, "Skybox",
		lua_pushboolean(L, c.enabled ? 1 : 0);
		lua_setfield(L, -2, "enabled"); lua_pushnumber(L, c.size);
		lua_setfield(L, -2, "size"); pushColourValue(L, c.dayTopColor);
		lua_setfield(L, -2, "dayTopColor");
		pushColourValue(L, c.dayBottomColor);
		lua_setfield(L, -2, "dayBottomColor");
		pushColourValue(L, c.nightTopColor);
		lua_setfield(L, -2, "nightTopColor");
		pushColourValue(L, c.nightBottomColor);
		lua_setfield(L, -2, "nightBottomColor");
		lua_pushboolean(L, c.starsEnabled ? 1 : 0);
		lua_setfield(L, -2, "starsEnabled");
		, if (lua_getfield(L, idx, "enabled"), lua_isboolean(L, -1))
			  c.enabled = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "size"), lua_isnumber(L, -1))
			c.size = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "dayTopColor"), lua_istable(L, -1))
			c.dayTopColor = readColourValue(L, lua_gettop(L));
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "dayBottomColor"), lua_istable(L, -1))
			c.dayBottomColor = readColourValue(L, lua_gettop(L));
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "nightTopColor"), lua_istable(L, -1))
			c.nightTopColor = readColourValue(L, lua_gettop(L));
		lua_pop(L, 1); if (lua_getfield(L, idx, "nightBottomColor"),
				   lua_istable(L, -1)) c.nightBottomColor =
				       readColourValue(L, lua_gettop(L));
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "starsEnabled"), lua_isboolean(L, -1))
			c.starsEnabled = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1););

	// --- WaterPlane ---
	REGISTER_COMPONENT(
		WaterPlane, "WaterPlane", lua_pushboolean(L, c.enabled ? 1 : 0);
		lua_setfield(L, -2, "enabled");
		lua_pushnumber(L, c.waterSurfaceY);
		lua_setfield(L, -2, "waterSurfaceY");
		lua_pushnumber(L, c.planeSize);
		lua_setfield(L, -2, "planeSize");
		pushColourValue(L, c.waterColor);
		lua_setfield(L, -2, "waterColor");
		lua_pushnumber(L, c.reflectivity);
		lua_setfield(L, -2, "reflectivity");
		lua_pushnumber(L, c.waveSpeed);
		lua_setfield(L, -2, "waveSpeed");
		, if (lua_getfield(L, idx, "enabled"), lua_isboolean(L, -1))
			  c.enabled = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "waterSurfaceY"), lua_isnumber(L, -1))
			c.waterSurfaceY = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "planeSize"), lua_isnumber(L, -1))
			c.planeSize = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "waterColor"), lua_istable(L, -1))
			c.waterColor = readColourValue(L, lua_gettop(L));
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "reflectivity"), lua_isnumber(L, -1))
			c.reflectivity = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "waveSpeed"), lua_isnumber(L, -1))
			c.waveSpeed = (float)lua_tonumber(L, -1);
		lua_pop(L, 1););

	// --- WaterPhysics ---
	REGISTER_COMPONENT(
		WaterPhysics, "WaterPhysics",
		lua_pushnumber(L, c.waterSurfaceY);
		lua_setfield(L, -2, "waterSurfaceY");
		lua_pushnumber(L, c.defaultBuoyancy);
		lua_setfield(L, -2, "defaultBuoyancy");
		lua_pushnumber(L, c.defaultLinearDrag);
		lua_setfield(L, -2, "defaultLinearDrag");
		lua_pushnumber(L, c.defaultAngularDrag);
		lua_setfield(L, -2, "defaultAngularDrag");
		lua_pushboolean(L, c.enabled ? 1 : 0);
		lua_setfield(L, -2, "enabled");
		,
		if (lua_getfield(L, idx, "waterSurfaceY"), lua_isnumber(L, -1))
			c.waterSurfaceY = (float)lua_tonumber(L, -1);
		lua_pop(L, 1); if (lua_getfield(L, idx, "defaultBuoyancy"),
				   lua_isnumber(L, -1)) c.defaultBuoyancy =
				       (float)lua_tonumber(L, -1);
		lua_pop(L, 1); if (lua_getfield(L, idx, "defaultLinearDrag"),
				   lua_isnumber(L, -1)) c.defaultLinearDrag =
				       (float)lua_tonumber(L, -1);
		lua_pop(L, 1); if (lua_getfield(L, idx, "defaultAngularDrag"),
				   lua_isnumber(L, -1)) c.defaultAngularDrag =
				       (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "enabled"), lua_isboolean(L, -1))
			c.enabled = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1););

	// --- BuoyancyInfo ---
	REGISTER_COMPONENT(
		BuoyancyInfo, "BuoyancyInfo",
		lua_pushboolean(L, c.enabled ? 1 : 0);
		lua_setfield(L, -2, "enabled"); lua_pushnumber(L, c.buoyancy);
		lua_setfield(L, -2, "buoyancy");
		lua_pushnumber(L, c.linearDrag);
		lua_setfield(L, -2, "linearDrag");
		lua_pushnumber(L, c.angularDrag);
		lua_setfield(L, -2, "angularDrag");
		lua_pushnumber(L, c.waterSurfaceY);
		lua_setfield(L, -2, "waterSurfaceY");
		lua_pushnumber(L, c.submergedThreshold);
		lua_setfield(L, -2, "submergedThreshold");
		, if (lua_getfield(L, idx, "enabled"), lua_isboolean(L, -1))
			  c.enabled = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "buoyancy"), lua_isnumber(L, -1))
			c.buoyancy = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "linearDrag"), lua_isnumber(L, -1))
			c.linearDrag = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "angularDrag"), lua_isnumber(L, -1))
			c.angularDrag = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "waterSurfaceY"), lua_isnumber(L, -1))
			c.waterSurfaceY = (float)lua_tonumber(L, -1);
		lua_pop(L, 1); if (lua_getfield(L, idx, "submergedThreshold"),
				   lua_isnumber(L, -1)) c.submergedThreshold =
				       (float)lua_tonumber(L, -1);
		lua_pop(L, 1););

	// --- InWater (tag) ---
	s_components["InWater"] = {
		"InWater",
		[](lua_State *L, flecs::entity e) {
			lua_pushboolean(L, e.has<InWater>() ? 1 : 0);
		},
		[](lua_State *L, flecs::entity e, int idx) {
			(void)idx;
			if (lua_toboolean(L, -1))
				e.add<InWater>();
			else
				e.remove<InWater>();
		}
	};

	// --- StartupMenu ---
	REGISTER_COMPONENT(
		StartupMenuComponent, "StartupMenu",
		lua_pushstring(L, c.fontName.c_str());
		lua_setfield(L, -2, "fontName"); lua_pushnumber(L, c.fontSize);
		lua_setfield(L, -2, "fontSize");
		lua_pushstring(L, c.newGameScene.c_str());
		lua_setfield(L, -2, "newGameScene");
		lua_pushboolean(L, c.showLoadGame ? 1 : 0);
		lua_setfield(L, -2, "showLoadGame");
		lua_pushboolean(L, c.showOptions ? 1 : 0);
		lua_setfield(L, -2, "showOptions");
		lua_pushboolean(L, c.showQuit ? 1 : 0);
		lua_setfield(L, -2, "showQuit");
		, if (lua_getfield(L, idx, "fontName"), lua_isstring(L, -1))
			  c.fontName = lua_tostring(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "fontSize"), lua_isnumber(L, -1))
			c.fontSize = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "newGameScene"), lua_isstring(L, -1))
			c.newGameScene = lua_tostring(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "showLoadGame"), lua_isboolean(L, -1))
			c.showLoadGame = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "showOptions"), lua_isboolean(L, -1))
			c.showOptions = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "showQuit"), lua_isboolean(L, -1))
			c.showQuit = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1););

	// --- PlayerController ---
	REGISTER_COMPONENT(
		PlayerControllerComponent, "PlayerController",
		lua_pushinteger(L, c.cameraMode);
		lua_setfield(L, -2, "cameraMode");
		lua_pushstring(L, c.targetCharacterName.c_str());
		lua_setfield(L, -2, "targetCharacterName");
		lua_pushnumber(L, c.tpsDistance);
		lua_setfield(L, -2, "tpsDistance");
		lua_pushnumber(L, c.tpsHeight);
		lua_setfield(L, -2, "tpsHeight");
		lua_pushnumber(L, c.mouseSensitivity);
		lua_setfield(L, -2, "mouseSensitivity");
		lua_pushnumber(L, c.actuatorDistance);
		lua_setfield(L, -2, "actuatorDistance");
		lua_pushnumber(L, c.actuatorCooldown);
		lua_setfield(L, -2, "actuatorCooldown");
		, if (lua_getfield(L, idx, "cameraMode"), lua_isnumber(L, -1))
			  c.cameraMode = (int)lua_tointeger(L, -1);
		lua_pop(L, 1); if (lua_getfield(L, idx, "targetCharacterName"),
				   lua_isstring(L, -1)) c.targetCharacterName =
				       lua_tostring(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "tpsDistance"), lua_isnumber(L, -1))
			c.tpsDistance = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "tpsHeight"), lua_isnumber(L, -1))
			c.tpsHeight = (float)lua_tonumber(L, -1);
		lua_pop(L, 1); if (lua_getfield(L, idx, "mouseSensitivity"),
				   lua_isnumber(L, -1)) c.mouseSensitivity =
				       (float)lua_tonumber(L, -1);
		lua_pop(L, 1); if (lua_getfield(L, idx, "actuatorDistance"),
				   lua_isnumber(L, -1)) c.actuatorDistance =
				       (float)lua_tonumber(L, -1);
		lua_pop(L, 1); if (lua_getfield(L, idx, "actuatorCooldown"),
				   lua_isnumber(L, -1)) c.actuatorCooldown =
				       (float)lua_tonumber(L, -1);
		lua_pop(L, 1););

	// --- CellGrid ---
	REGISTER_COMPONENT(
		CellGridComponent, "CellGrid", lua_pushinteger(L, c.width);
		lua_setfield(L, -2, "width"); lua_pushinteger(L, c.height);
		lua_setfield(L, -2, "height"); lua_pushinteger(L, c.depth);
		lua_setfield(L, -2, "depth"); lua_pushnumber(L, c.cellSize);
		lua_setfield(L, -2, "cellSize");
		lua_pushnumber(L, c.cellHeight);
		lua_setfield(L, -2, "cellHeight");
		, if (lua_getfield(L, idx, "width"), lua_isnumber(L, -1))
			  c.width = (int)lua_tointeger(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "height"), lua_isnumber(L, -1))
			c.height = (int)lua_tointeger(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "depth"), lua_isnumber(L, -1))
			c.depth = (int)lua_tointeger(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "cellSize"), lua_isnumber(L, -1))
			c.cellSize = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "cellHeight"), lua_isnumber(L, -1))
			c.cellHeight = (float)lua_tonumber(L, -1);
		lua_pop(L, 1););

	// --- Room ---
	REGISTER_COMPONENT(
		RoomComponent, "Room", lua_pushinteger(L, c.minX);
		lua_setfield(L, -2, "minX"); lua_pushinteger(L, c.minY);
		lua_setfield(L, -2, "minY"); lua_pushinteger(L, c.minZ);
		lua_setfield(L, -2, "minZ"); lua_pushinteger(L, c.maxX);
		lua_setfield(L, -2, "maxX"); lua_pushinteger(L, c.maxY);
		lua_setfield(L, -2, "maxY"); lua_pushinteger(L, c.maxZ);
		lua_setfield(L, -2, "maxZ");
		lua_pushstring(L, c.roomType.c_str());
		lua_setfield(L, -2, "roomType");
		lua_pushboolean(L, c.createFloor ? 1 : 0);
		lua_setfield(L, -2, "createFloor");
		lua_pushboolean(L, c.createCeiling ? 1 : 0);
		lua_setfield(L, -2, "createCeiling");
		lua_pushboolean(L, c.createInteriorWalls ? 1 : 0);
		lua_setfield(L, -2, "createInteriorWalls");
		lua_pushboolean(L, c.createWindows ? 1 : 0);
		lua_setfield(L, -2, "createWindows");
		, if (lua_getfield(L, idx, "minX"), lua_isnumber(L, -1))
			  c.minX = (int)lua_tointeger(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "minY"), lua_isnumber(L, -1))
			c.minY = (int)lua_tointeger(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "minZ"), lua_isnumber(L, -1))
			c.minZ = (int)lua_tointeger(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "maxX"), lua_isnumber(L, -1))
			c.maxX = (int)lua_tointeger(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "maxY"), lua_isnumber(L, -1))
			c.maxY = (int)lua_tointeger(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "maxZ"), lua_isnumber(L, -1))
			c.maxZ = (int)lua_tointeger(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "roomType"), lua_isstring(L, -1))
			c.roomType = lua_tostring(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "createFloor"), lua_isboolean(L, -1))
			c.createFloor = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "createCeiling"), lua_isboolean(L, -1))
			c.createCeiling = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1); if (lua_getfield(L, idx, "createInteriorWalls"),
				   lua_isboolean(L, -1)) c.createInteriorWalls =
				       lua_toboolean(L, -1) != 0;
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "createWindows"), lua_isboolean(L, -1))
			c.createWindows = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1););

	// --- ClearArea ---
	REGISTER_COMPONENT(
		ClearAreaComponent, "ClearArea", lua_pushinteger(L, c.minX);
		lua_setfield(L, -2, "minX"); lua_pushinteger(L, c.minY);
		lua_setfield(L, -2, "minY"); lua_pushinteger(L, c.minZ);
		lua_setfield(L, -2, "minZ"); lua_pushinteger(L, c.maxX);
		lua_setfield(L, -2, "maxX"); lua_pushinteger(L, c.maxY);
		lua_setfield(L, -2, "maxY"); lua_pushinteger(L, c.maxZ);
		lua_setfield(L, -2, "maxZ");
		lua_pushboolean(L, c.clearCells ? 1 : 0);
		lua_setfield(L, -2, "clearCells");
		lua_pushboolean(L, c.clearFurniture ? 1 : 0);
		lua_setfield(L, -2, "clearFurniture");
		, if (lua_getfield(L, idx, "minX"), lua_isnumber(L, -1))
			  c.minX = (int)lua_tointeger(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "minY"), lua_isnumber(L, -1))
			c.minY = (int)lua_tointeger(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "minZ"), lua_isnumber(L, -1))
			c.minZ = (int)lua_tointeger(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "maxX"), lua_isnumber(L, -1))
			c.maxX = (int)lua_tointeger(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "maxY"), lua_isnumber(L, -1))
			c.maxY = (int)lua_tointeger(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "maxZ"), lua_isnumber(L, -1))
			c.maxZ = (int)lua_tointeger(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "clearCells"), lua_isboolean(L, -1))
			c.clearCells = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1); if (lua_getfield(L, idx, "clearFurniture"),
				   lua_isboolean(L, -1)) c.clearFurniture =
				       lua_toboolean(L, -1) != 0;
		lua_pop(L, 1););

	// --- Roof ---
	REGISTER_COMPONENT(
		RoofComponent, "Roof", lua_pushinteger(L, (int)c.type);
		lua_setfield(L, -2, "type"); lua_pushinteger(L, c.posX);
		lua_setfield(L, -2, "posX"); lua_pushinteger(L, c.posY);
		lua_setfield(L, -2, "posY"); lua_pushinteger(L, c.posZ);
		lua_setfield(L, -2, "posZ"); lua_pushinteger(L, c.sizeX);
		lua_setfield(L, -2, "sizeX"); lua_pushinteger(L, c.sizeZ);
		lua_setfield(L, -2, "sizeZ"); lua_pushnumber(L, c.baseHeight);
		lua_setfield(L, -2, "baseHeight");
		lua_pushnumber(L, c.maxHeight);
		lua_setfield(L, -2, "maxHeight");
		, if (lua_getfield(L, idx, "type"), lua_isnumber(L, -1))
			  c.type = (RoofComponent::Type)lua_tointeger(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "posX"), lua_isnumber(L, -1))
			c.posX = (int)lua_tointeger(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "posY"), lua_isnumber(L, -1))
			c.posY = (int)lua_tointeger(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "posZ"), lua_isnumber(L, -1))
			c.posZ = (int)lua_tointeger(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "sizeX"), lua_isnumber(L, -1))
			c.sizeX = (int)lua_tointeger(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "sizeZ"), lua_isnumber(L, -1))
			c.sizeZ = (int)lua_tointeger(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "baseHeight"), lua_isnumber(L, -1))
			c.baseHeight = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "maxHeight"), lua_isnumber(L, -1))
			c.maxHeight = (float)lua_tonumber(L, -1);
		lua_pop(L, 1););

	// --- Lot ---
	REGISTER_COMPONENT(
		LotComponent, "Lot", lua_pushinteger(L, c.width);
		lua_setfield(L, -2, "width"); lua_pushinteger(L, c.depth);
		lua_setfield(L, -2, "depth"); lua_pushnumber(L, c.elevation);
		lua_setfield(L, -2, "elevation"); lua_pushnumber(L, c.angle);
		lua_setfield(L, -2, "angle");
		lua_pushstring(L, c.templateName.c_str());
		lua_setfield(L, -2, "templateName");
		, if (lua_getfield(L, idx, "width"), lua_isnumber(L, -1))
			  c.width = (int)lua_tointeger(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "depth"), lua_isnumber(L, -1))
			c.depth = (int)lua_tointeger(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "elevation"), lua_isnumber(L, -1))
			c.elevation = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "angle"), lua_isnumber(L, -1))
			c.angle = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "templateName"), lua_isstring(L, -1))
			c.templateName = lua_tostring(L, -1);
		lua_pop(L, 1););

	// --- District ---
	REGISTER_COMPONENT(
		DistrictComponent, "District", lua_pushnumber(L, c.radius);
		lua_setfield(L, -2, "radius"); lua_pushnumber(L, c.elevation);
		lua_setfield(L, -2, "elevation"); lua_pushnumber(L, c.height);
		lua_setfield(L, -2, "height");
		lua_pushboolean(L, c.isPlaza ? 1 : 0);
		lua_setfield(L, -2, "isPlaza");
		, if (lua_getfield(L, idx, "radius"), lua_isnumber(L, -1))
			  c.radius = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "elevation"), lua_isnumber(L, -1))
			c.elevation = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "height"), lua_isnumber(L, -1))
			c.height = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "isPlaza"), lua_isboolean(L, -1))
			c.isPlaza = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1););

	// --- Town ---
	REGISTER_COMPONENT(
		TownComponent, "Town", lua_pushstring(L, c.townName.c_str());
		lua_setfield(L, -2, "townName");
		, if (lua_getfield(L, idx, "townName"), lua_isstring(L, -1))
			  c.townName = lua_tostring(L, -1);
		lua_pop(L, 1););

	// --- FurnitureTemplate ---
	REGISTER_COMPONENT(
		FurnitureTemplateComponent, "FurnitureTemplate",
		lua_pushstring(L, c.templateName.c_str());
		lua_setfield(L, -2, "templateName");
		lua_pushstring(L, c.meshName.c_str());
		lua_setfield(L, -2, "meshName");
		lua_pushstring(L, c.materialName.c_str());
		lua_setfield(L, -2, "materialName");
		lua_pushboolean(L, c.requiresWall ? 1 : 0);
		lua_setfield(L, -2, "requiresWall");
		lua_pushboolean(L, c.requiresFloor ? 1 : 0);
		lua_setfield(L, -2, "requiresFloor");
		lua_pushboolean(L, c.blocksPath ? 1 : 0);
		lua_setfield(L, -2, "blocksPath"); lua_pushnumber(L, c.weight);
		lua_setfield(L, -2, "weight");
		, if (lua_getfield(L, idx, "templateName"), lua_isstring(L, -1))
			  c.templateName = lua_tostring(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "meshName"), lua_isstring(L, -1))
			c.meshName = lua_tostring(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "materialName"), lua_isstring(L, -1))
			c.materialName = lua_tostring(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "requiresWall"), lua_isboolean(L, -1))
			c.requiresWall = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "requiresFloor"), lua_isboolean(L, -1))
			c.requiresFloor = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "blocksPath"), lua_isboolean(L, -1))
			c.blocksPath = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "weight"), lua_isnumber(L, -1))
			c.weight = (float)lua_tonumber(L, -1);
		lua_pop(L, 1););

	// --- PrefabInstance ---
	REGISTER_COMPONENT(
		PrefabInstanceComponent, "PrefabInstance",
		lua_pushstring(L, c.prefabPath.c_str());
		lua_setfield(L, -2, "prefabPath");
		lua_pushboolean(L, c.instantiated ? 1 : 0);
		lua_setfield(L, -2, "instantiated");
		, if (lua_getfield(L, idx, "prefabPath"), lua_isstring(L, -1))
			  c.prefabPath = lua_tostring(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "instantiated"), lua_isboolean(L, -1))
			c.instantiated = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1););

	// --- GoapPlanner ---
	REGISTER_COMPONENT(
		GoapPlannerComponent, "GoapPlanner",
		pushStringVector(L, c.actionNames);
		lua_setfield(L, -2, "actionNames");
		pushStringVector(L, c.goalNames);
		lua_setfield(L, -2, "goalNames");
		lua_pushnumber(L, c.smartObjectDistance);
		lua_setfield(L, -2, "smartObjectDistance");
		lua_pushboolean(L, c.includeSmartObjects ? 1 : 0);
		lua_setfield(L, -2, "includeSmartObjects");
		lua_pushstring(L, c.actionDatabaseRef.c_str());
		lua_setfield(L, -2, "actionDatabaseRef");
		lua_pushboolean(L, c.planDirty ? 1 : 0);
		lua_setfield(L, -2, "planDirty");
		lua_pushinteger(L, c.maxPlans); lua_setfield(L, -2, "maxPlans");
		, if (lua_getfield(L, idx, "actionNames"), lua_istable(L, -1))
			  c.actionNames = readStringVector(L, lua_gettop(L));
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "goalNames"), lua_istable(L, -1))
			c.goalNames = readStringVector(L, lua_gettop(L));
		lua_pop(L, 1); if (lua_getfield(L, idx, "smartObjectDistance"),
				   lua_isnumber(L, -1)) c.smartObjectDistance =
				       (float)lua_tonumber(L, -1);
		lua_pop(L, 1); if (lua_getfield(L, idx, "includeSmartObjects"),
				   lua_isboolean(L, -1)) c.includeSmartObjects =
				       lua_toboolean(L, -1) != 0;
		lua_pop(L, 1); if (lua_getfield(L, idx, "actionDatabaseRef"),
				   lua_isstring(L, -1)) c.actionDatabaseRef =
				       lua_tostring(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "planDirty"), lua_isboolean(L, -1))
			c.planDirty = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "maxPlans"), lua_isnumber(L, -1))
			c.maxPlans = (int)lua_tointeger(L, -1);
		lua_pop(L, 1););

	// --- GoapRunner ---
	REGISTER_COMPONENT(
		GoapRunnerComponent, "GoapRunner",
		lua_pushinteger(L, (int)c.state);
		lua_setfield(L, -2, "state");
		lua_pushinteger(L, c.currentActionIndex);
		lua_setfield(L, -2, "currentActionIndex");
		lua_pushstring(L, c.currentActionName.c_str());
		lua_setfield(L, -2, "currentActionName");
		lua_pushnumber(L, c.actionTimer);
		lua_setfield(L, -2, "actionTimer");
		lua_pushboolean(L, c.autoReplan ? 1 : 0);
		lua_setfield(L, -2, "autoReplan");
		, if (lua_getfield(L, idx, "state"), lua_isnumber(L, -1))
			  c.state =
			  (GoapRunnerComponent::State)lua_tointeger(L, -1);
		lua_pop(L, 1); if (lua_getfield(L, idx, "currentActionIndex"),
				   lua_isnumber(L, -1)) c.currentActionIndex =
				       (int)lua_tointeger(L, -1);
		lua_pop(L, 1); if (lua_getfield(L, idx, "currentActionName"),
				   lua_isstring(L, -1)) c.currentActionName =
				       lua_tostring(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "actionTimer"), lua_isnumber(L, -1))
			c.actionTimer = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "autoReplan"), lua_isboolean(L, -1))
			c.autoReplan = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1););

	// ActionDatabase is now a singleton, not a per-entity component.
	// Use ActionDatabase:getSingleton() from Lua API instead.

	// --- ActionDebug ---
	REGISTER_COMPONENT(
		ActionDebug, "ActionDebug",
		lua_pushboolean(L, c.isRunning ? 1 : 0);
		lua_setfield(L, -2, "isRunning");
		lua_pushstring(L, c.selectedActionName.c_str());
		lua_setfield(L, -2, "selectedActionName");
		lua_pushstring(L, c.selectedGoalName.c_str());
		lua_setfield(L, -2, "selectedGoalName");
		lua_pushstring(L, c.currentActionName.c_str());
		lua_setfield(L, -2, "currentActionName");
		lua_pushstring(L, c.lastResult.c_str());
		lua_setfield(L, -2, "lastResult");
		, if (lua_getfield(L, idx, "isRunning"), lua_isboolean(L, -1))
			  c.isRunning = lua_toboolean(L, -1) != 0;
		lua_pop(L, 1); if (lua_getfield(L, idx, "selectedActionName"),
				   lua_isstring(L, -1)) c.selectedActionName =
				       lua_tostring(L, -1);
		lua_pop(L, 1); if (lua_getfield(L, idx, "selectedGoalName"),
				   lua_isstring(L, -1))
				       c.selectedGoalName = lua_tostring(L, -1);
		lua_pop(L, 1); if (lua_getfield(L, idx, "currentActionName"),
				   lua_isstring(L, -1)) c.currentActionName =
				       lua_tostring(L, -1);
		lua_pop(L, 1);
		if (lua_getfield(L, idx, "lastResult"), lua_isstring(L, -1))
			c.lastResult = lua_tostring(L, -1);
		lua_pop(L, 1););

	// --- GeneratedPhysicsTag (tag, no fields) ---
	s_components["GeneratedPhysicsTag"] = {
		"GeneratedPhysicsTag",
		[](lua_State *L, flecs::entity e) {
			lua_pushboolean(L,
					e.has<GeneratedPhysicsTag>() ? 1 : 0);
		},
		[](lua_State *L, flecs::entity e, int idx) {
			(void)idx;
			if (lua_toboolean(L, -1))
				e.add<GeneratedPhysicsTag>();
			else
				e.remove<GeneratedPhysicsTag>();
		}
	};

	// --- ParentComponent ---
	// Note: ParentComponent stores a flecs::entity which cannot be
	// directly serialized to Lua. We expose it as a tag (has/doesn't have)
	// and provide the parent entity via ecs.parent() in the entity API.
	s_components["ParentComponent"] = {
		"ParentComponent",
		[](lua_State *L, flecs::entity e) {
			lua_pushboolean(L, e.has<ParentComponent>() ? 1 : 0);
		},
		[](lua_State *L, flecs::entity e, int idx) {
			(void)idx;
			if (lua_toboolean(L, -1))
				e.add<ParentComponent>();
			else
				e.remove<ParentComponent>();
		}
	};

	// --- ModifiedComponent ---
	s_components["ModifiedComponent"] = {
		"ModifiedComponent",
		[](lua_State *L, flecs::entity e) {
			if (!e.has<ModifiedComponent>()) {
				lua_pushnil(L);
				return;
			}
			const auto &c = e.get<ModifiedComponent>();
			lua_newtable(L);
			lua_pushboolean(L, c.modified ? 1 : 0);
			lua_setfield(L, -2, "modified");
		},
		[](lua_State *L, flecs::entity e, int idx) {
			ModifiedComponent c;
			if (e.has<ModifiedComponent>())
				c = e.get<ModifiedComponent>();
			if (lua_istable(L, idx)) {
				if (lua_getfield(L, idx, "modified"),
				    lua_isboolean(L, -1))
					c.modified = lua_toboolean(L, -1) != 0;
				lua_pop(L, 1);
			}
			e.set<ModifiedComponent>(c);
		}
	};
}

// ---------------------------------------------------------------------------
// Helper: get a Flecs entity from a Lua integer ID on the stack
// ---------------------------------------------------------------------------

/**
 * @brief Read a Lua integer entity ID from the stack and return the
 *        corresponding Flecs entity.
 *
 * The first argument on the Lua stack must be an integer entity ID
 * that was previously returned by ecs.create_entity().
 *
 * @param L Lua state.
 * @return flecs::entity The corresponding Flecs entity.
 */
static flecs::entity getEntity(lua_State *L)
{
	int id = (int)luaL_checkinteger(L, 1);
	return luaIdToEntity(id);
}

// ---------------------------------------------------------------------------
// Lua C-callable functions
// ---------------------------------------------------------------------------

/**
 * @brief Lua: ecs.add_component(entity_id, "ComponentName")
 *
 * Adds a component to an entity. If the component has fields, they are
 * initialized to defaults.
 */
static int luaAddComponent(lua_State *L)
{
	flecs::entity e = getEntity(L);
	const char *name = luaL_checkstring(L, 2);

	auto it = s_components.find(name);
	if (it == s_components.end()) {
		luaL_error(L, "Unknown component: %s", name);
		return 0;
	}

	// Add the component by calling readFields with an empty table
	// to set defaults, then pushFields to get the component
	it->second.readFields(L, e, -1);
	return 0;
}

/**
 * @brief Lua: ecs.remove_component(entity_id, "ComponentName")
 *
 * Removes a component from an entity.
 */
static int luaRemoveComponent(lua_State *L)
{
	flecs::entity e = getEntity(L);
	const char *name = luaL_checkstring(L, 2);

	auto it = s_components.find(name);
	if (it == s_components.end()) {
		luaL_error(L, "Unknown component: %s", name);
		return 0;
	}

	// Remove by calling readFields with nil to clear
	lua_pushnil(L);
	it->second.readFields(L, e, lua_gettop(L));
	lua_pop(L, 1);
	return 0;
}

/**
 * @brief Lua: ecs.has_component(entity_id, "ComponentName") -> bool
 *
 * Checks if an entity has a component.
 */
static int luaHasComponent(lua_State *L)
{
	flecs::entity e = getEntity(L);
	const char *name = luaL_checkstring(L, 2);

	auto it = s_components.find(name);
	if (it == s_components.end()) {
		lua_pushboolean(L, 0);
		return 1;
	}

	it->second.pushFields(L, e);
	// pushFields pushes nil if not present, or a table if present
	bool has = !lua_isnil(L, -1);
	lua_pop(L, 1);
	lua_pushboolean(L, has ? 1 : 0);
	return 1;
}

/**
 * @brief Lua: ecs.get_component(entity_id, "ComponentName") -> table
 *
 * Returns a table of all fields for a component, or nil if not present.
 */
static int luaGetComponent(lua_State *L)
{
	flecs::entity e = getEntity(L);
	const char *name = luaL_checkstring(L, 2);

	auto it = s_components.find(name);
	if (it == s_components.end()) {
		lua_pushnil(L);
		return 1;
	}

	it->second.pushFields(L, e);
	return 1;
}

/**
 * @brief Lua: ecs.set_component(entity_id, "ComponentName", table)
 *
 * Sets all fields of a component from a Lua table.
 */
static int luaSetComponent(lua_State *L)
{
	flecs::entity e = getEntity(L);
	const char *name = luaL_checkstring(L, 2);
	luaL_checktype(L, 3, LUA_TTABLE);

	auto it = s_components.find(name);
	if (it == s_components.end()) {
		luaL_error(L, "Unknown component: %s", name);
		return 0;
	}

	it->second.readFields(L, e, 3);
	return 0;
}

/**
 * @brief Lua: ecs.get_field(entity_id, "ComponentName", "fieldName") -> value
 *
 * Gets a single field value from a component.
 */
static int luaGetField(lua_State *L)
{
	flecs::entity e = getEntity(L);
	const char *name = luaL_checkstring(L, 2);
	const char *field = luaL_checkstring(L, 3);

	auto it = s_components.find(name);
	if (it == s_components.end()) {
		lua_pushnil(L);
		return 1;
	}

	// Push the component table, then get the field
	it->second.pushFields(L, e);
	if (lua_isnil(L, -1)) {
		return 1;
	}
	lua_getfield(L, -1, field);
	// Remove the component table, keep the field value
	lua_remove(L, -2);
	return 1;
}

/**
 * @brief Lua: ecs.set_field(entity_id, "ComponentName", "fieldName", value)
 *
 * Sets a single field value on a component.
 */
static int luaSetField(lua_State *L)
{
	flecs::entity e = getEntity(L);
	const char *name = luaL_checkstring(L, 2);
	const char *field = luaL_checkstring(L, 3);

	auto it = s_components.find(name);
	if (it == s_components.end()) {
		luaL_error(L, "Unknown component: %s", name);
		return 0;
	}

	// Get current component as table, set the field, write back
	it->second.pushFields(L, e);
	if (lua_isnil(L, -1)) {
		// Component doesn't exist yet, create a new table
		lua_newtable(L);
	}
	// Copy value from stack position 4 into the table
	lua_pushvalue(L, 4);
	lua_setfield(L, -2, field);
	// Write back
	it->second.readFields(L, e, lua_gettop(L));
	lua_pop(L, 1);
	return 0;
}

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

void registerLuaComponentApi(lua_State *L)
{
	registerAllComponents();

	// 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 functions
	lua_pushcfunction(L, luaAddComponent);
	lua_setfield(L, -2, "add_component");

	lua_pushcfunction(L, luaRemoveComponent);
	lua_setfield(L, -2, "remove_component");

	lua_pushcfunction(L, luaHasComponent);
	lua_setfield(L, -2, "has_component");

	lua_pushcfunction(L, luaGetComponent);
	lua_setfield(L, -2, "get_component");

	lua_pushcfunction(L, luaSetComponent);
	lua_setfield(L, -2, "set_component");

	lua_pushcfunction(L, luaGetField);
	lua_setfield(L, -2, "get_field");

	lua_pushcfunction(L, luaSetField);
	lua_setfield(L, -2, "set_field");

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

} // namespace editScene
