
// LuaState.cpp

// Implements the cLuaState class representing the wrapper over lua_State *, provides associated helper functions

#include "Globals.h"
#include "LuaState.h"

extern "C"
{
	#include "lua/src/lualib.h"
}

#undef TOLUA_TEMPLATE_BIND
#include "tolua++/include/tolua++.h"
#include "Bindings.h"
#include "ManualBindings.h"
#include "DeprecatedBindings.h"
#include "LuaJson.h"
#include "../Entities/Entity.h"
#include "../BlockEntities/BlockEntity.h"
#include "../DeadlockDetect.h"
#include "../UUID.h"





// Hotpatching the Macro to prevent a Clang Warning (0 for pointer used)
#undef  lua_tostring
#define lua_tostring(L, i) lua_tolstring(L, (i), nullptr)





// fwd: "SQLite/lsqlite3.cpp"
extern "C"
{
	int luaopen_lsqlite3(lua_State * L);
}

// fwd: "LuaExpat/lxplib.cpp":
int luaopen_lxp(lua_State * L);





const cLuaState::cRet cLuaState::Return = {};
const cLuaState::cNil cLuaState::Nil = {};





////////////////////////////////////////////////////////////////////////////////
// cCanonLuaStates:

/** Tracks the canon cLuaState instances for each lua_State pointer.
Used for tracked refs - the ref needs to be tracked by a single cLuaState (the canon state), despite being created from a different (attached) cLuaState.
The canon state must be available without accessing the Lua state itself (so it cannot be stored within Lua). */
class cCanonLuaStates
{
public:
	/** Returns the canon Lua state for the specified lua_State pointer. */
	static cLuaState * GetCanonState(lua_State * a_LuaState)
	{
		auto & inst = GetInstance();
		cCSLock lock(inst.m_CS);
		auto itr = inst.m_CanonStates.find(a_LuaState);
		if (itr == inst.m_CanonStates.end())
		{
			return nullptr;
		}
		return itr->second;
	}

	/** Adds a new canon cLuaState instance to the map.
	Used when a new Lua state is created, this informs the map that a new canon Lua state should be tracked. */
	static void Add(cLuaState & a_LuaState)
	{
		auto & inst = GetInstance();
		cCSLock lock(inst.m_CS);
		ASSERT(inst.m_CanonStates.find(a_LuaState) == inst.m_CanonStates.end());
		inst.m_CanonStates[a_LuaState.operator lua_State *()] = &a_LuaState;
	}

	/** Removes the bindings between the specified canon state and its lua_State pointer.
	Used when a Lua state is being closed. */
	static void Remove(cLuaState & a_LuaState)
	{
		auto & inst = GetInstance();
		cCSLock lock(inst.m_CS);
		auto itr = inst.m_CanonStates.find(a_LuaState);
		ASSERT(itr != inst.m_CanonStates.end());
		inst.m_CanonStates.erase(itr);
	}

protected:
	/** The mutex protecting m_CanonStates against multithreaded access. */
	cCriticalSection m_CS;

	/** Map of lua_State pointers to their canon cLuaState instances. */
	std::map<lua_State *, cLuaState *> m_CanonStates;


	/** Returns the singleton instance of this class. */
	static cCanonLuaStates & GetInstance(void)
	{
		static cCanonLuaStates canonLuaStates;
		return canonLuaStates;
	}
};





////////////////////////////////////////////////////////////////////////////////
// cLuaStateTracker:

void cLuaStateTracker::Add(cLuaState & a_LuaState)
{
	auto & Instance = Get();
	cCSLock Lock(Instance.m_CSLuaStates);
	Instance.m_LuaStates.push_back(&a_LuaState);
}





void cLuaStateTracker::Del(cLuaState & a_LuaState)
{
	auto & Instance = Get();
	cCSLock Lock(Instance.m_CSLuaStates);
	Instance.m_LuaStates.erase(
		std::remove_if(
			Instance.m_LuaStates.begin(), Instance.m_LuaStates.end(),
			[&a_LuaState](cLuaStatePtr a_StoredLuaState)
			{
				return (&a_LuaState == a_StoredLuaState);
			}
		),
		Instance.m_LuaStates.end()
	);
}





AString cLuaStateTracker::GetStats()
{
	auto & Instance = Get();
	cCSLock Lock(Instance.m_CSLuaStates);
	AString res;
	int Total = 0;
	for (auto state: Instance.m_LuaStates)
	{
		int Mem = 0;
		if (!state->Call("collectgarbage", "count", cLuaState::Return, Mem))
		{
			res.append(fmt::format(FMT_STRING("Cannot query memory for state \"{}\"\n"), state->GetSubsystemName()));
		}
		else
		{
			res.append(fmt::format(FMT_STRING("State \"{}\" is using {} KiB of memory\n"), state->GetSubsystemName(), Mem));
			Total += Mem;
		}
	}
	res.append(fmt::format(FMT_STRING("Total memory used by Lua: {} KiB\n"), Total));
	return res;
}





cLuaStateTracker & cLuaStateTracker::Get()
{
	static cLuaStateTracker Inst;  // The singleton
	return Inst;
}





////////////////////////////////////////////////////////////////////////////////
// cLuaState::cTrackedRef:

cLuaState::cTrackedRef::cTrackedRef(void):
	m_CS(nullptr)
{
}





bool cLuaState::cTrackedRef::RefStack(cLuaState & a_LuaState, int a_StackPos)
{
	// Clear any previous callback:
	Clear();

	// Add self to LuaState's callback-tracking:
	auto canonState = a_LuaState.QueryCanonLuaState();
	canonState->TrackRef(*this);

	// Store the new callback:
	m_CS = &(canonState->m_CS);
	m_Ref.RefStack(*canonState, a_StackPos);
	return true;
}





void cLuaState::cTrackedRef::Clear(void)
{
	// Free the reference:
	lua_State * luaState = nullptr;
	{
		auto cs = m_CS.load();
		if (cs != nullptr)
		{
			cCSLock Lock(*cs);
			if (!m_Ref.IsValid())
			{
				return;
			}
			luaState = m_Ref.GetLuaState();
			m_Ref.UnRef();
		}
	}
	m_CS = nullptr;

	// Remove from LuaState's callback-tracking:
	if (luaState == nullptr)
	{
		return;
	}
	cLuaState(luaState).UntrackRef(*this);
}





bool cLuaState::cTrackedRef::IsValid(void)
{
	auto cs = m_CS.load();
	if (cs == nullptr)
	{
		return false;
	}
	cCSLock lock(*cs);
	return m_Ref.IsValid();
}





bool cLuaState::cTrackedRef::IsSameLuaState(cLuaState & a_LuaState)
{
	auto cs = m_CS.load();
	if (cs == nullptr)
	{
		return false;
	}
	cCSLock lock(*cs);
	if (!m_Ref.IsValid())
	{
		return false;
	}
	auto canonState = a_LuaState.QueryCanonLuaState();
	if (canonState == nullptr)
	{
		return false;
	}
	return (m_Ref.GetLuaState() == static_cast<lua_State *>(*canonState));
}





void cLuaState::cTrackedRef::Invalidate(void)
{
	auto cs = m_CS.load();
	if (cs == nullptr)
	{
		// Already invalid
		return;
	}
	cCSLock Lock(*cs);
	if (!m_Ref.IsValid())
	{
		LOGD("%s: Inconsistent callback at %p, has a CS but an invalid Ref. This should not happen",
			__FUNCTION__, static_cast<void *>(this)
		);
		return;
	}
	m_Ref.UnRef();
	m_CS = nullptr;
}





////////////////////////////////////////////////////////////////////////////////
// cLuaState::cCallback:

bool cLuaState::cCallback::RefStack(cLuaState & a_LuaState, int a_StackPos)
{
	// Check if the stack contains a function:
	if (!lua_isfunction(a_LuaState, a_StackPos))
	{
		return false;
	}

	return Super::RefStack(a_LuaState, a_StackPos);
}





////////////////////////////////////////////////////////////////////////////////
// cLuaState::cOptionalCallback:

bool cLuaState::cOptionalCallback::RefStack(cLuaState & a_LuaState, int a_StackPos)
{
	// If the stack pos is nil, make this an empty callback:
	if (lua_isnil(a_LuaState, a_StackPos))
	{
		Clear();
		return true;
	}

	// Use default cCallback implementation:
	return Super::RefStack(a_LuaState, a_StackPos);
}





////////////////////////////////////////////////////////////////////////////////
// cLuaState::cTableRef:

bool cLuaState::cTableRef::RefStack(cLuaState & a_LuaState, int a_StackPos)
{
	// Check if the stack contains a table:
	if (!lua_istable(a_LuaState, a_StackPos))
	{
		return false;
	}

	return Super::RefStack(a_LuaState, a_StackPos);
}





////////////////////////////////////////////////////////////////////////////////
// cLuaState::cStackTable:

cLuaState::cStackTable::cStackTable(cLuaState & a_LuaState, int a_StackPos):
	m_LuaState(a_LuaState),
	m_StackPos(a_StackPos)
{
	ASSERT(lua_istable(a_LuaState, a_StackPos));
}





void cLuaState::cStackTable::ForEachArrayElement(cFunctionRef<bool(cLuaState & a_LuaState, int a_Index)> a_ElementCallback) const
{
	const auto numElements = luaL_getn(m_LuaState, m_StackPos);
	[[maybe_unused]] const auto stackTop = lua_gettop(m_LuaState);
	for (int idx = 1; idx <= numElements; idx++)
	{
		// Push the idx-th element of the array onto stack top and call the callback:
		lua_rawgeti(m_LuaState, m_StackPos, idx);
		auto shouldAbort = a_ElementCallback(m_LuaState, idx);
		ASSERT(lua_gettop(m_LuaState) == stackTop + 1);  // The element callback must not change the Lua stack below the value
		lua_pop(m_LuaState, 1);
		if (shouldAbort)
		{
			// The callback wants to abort
			return;
		}
	}
}





void cLuaState::cStackTable::ForEachElement(cFunctionRef<bool(cLuaState & a_LuaState)> a_ElementCallback) const
{
	[[maybe_unused]] const auto stackTop = lua_gettop(m_LuaState);
	lua_pushvalue(m_LuaState, m_StackPos);  // Stk: <table>
	lua_pushnil(m_LuaState);                // Stk: <table> nil
	while (lua_next(m_LuaState, -2))        // Stk: <table> <key> <val>
	{
		auto shouldAbort = a_ElementCallback(m_LuaState);
		ASSERT(lua_gettop(m_LuaState) == stackTop + 3);  // The element callback must not change the Lua stack below the value
		lua_pop(m_LuaState, 1);  // Stk: <table> <key>
		if (shouldAbort)
		{
			// The callback wants to abort
			lua_pop(m_LuaState, 2);  // Stk: empty
			return;
		}
	}
	// Stk: <table>
	lua_pop(m_LuaState, 1);  // Stk: empty
}





////////////////////////////////////////////////////////////////////////////////
// cLuaState:

cLuaState::cLuaState(const AString & a_SubsystemName) :
	m_LuaState(nullptr),
	m_IsOwned(false),
	m_SubsystemName(a_SubsystemName),
	m_NumCurrentFunctionArgs(-1)
{
}





cLuaState::cLuaState(lua_State * a_AttachState) :
	m_LuaState(a_AttachState),
	m_IsOwned(false),
	m_SubsystemName("<attached>"),
	m_NumCurrentFunctionArgs(-1)
{
}





cLuaState::~cLuaState()
{
	if (IsValid())
	{
		if (m_IsOwned)
		{
			Close();
		}
		else
		{
			Detach();
		}
	}
}





void cLuaState::Create(void)
{
	if (m_LuaState != nullptr)
	{
		LOGWARNING("%s: Trying to create an already-existing LuaState, ignoring.", __FUNCTION__);
		return;
	}
	m_LuaState = lua_open();
	luaL_openlibs(m_LuaState);
	m_IsOwned = true;
	cLuaStateTracker::Add(*this);
	cCanonLuaStates::Add(*this);
}





void cLuaState::RegisterAPILibs(void)
{
	auto top = lua_gettop(m_LuaState);
	tolua_AllToLua_open(m_LuaState);
	ASSERT(top == lua_gettop(m_LuaState));
	cManualBindings::Bind(m_LuaState);
	ASSERT(top == lua_gettop(m_LuaState));
	DeprecatedBindings::Bind(m_LuaState);
	ASSERT(top == lua_gettop(m_LuaState));
	cLuaJson::Bind(*this);
	ASSERT(top == lua_gettop(m_LuaState));
	luaopen_lsqlite3(m_LuaState);
	if (top == lua_gettop(m_LuaState) - 1)
	{
		// lsqlite3 left the "sqlite3" table on the stack, pop it:
		lua_pop(m_LuaState, 1);
	}
	ASSERT(top == lua_gettop(m_LuaState));
	luaopen_lxp(m_LuaState);
	if (top == lua_gettop(m_LuaState) - 1)
	{
		// lxp left the unregistered "lxp" table on the stack, register and pop it (#3304):
		lua_setglobal(m_LuaState, "lxp");
	}
	ASSERT(top == lua_gettop(m_LuaState));
}





void cLuaState::Close(void)
{
	if (m_LuaState == nullptr)
	{
		LOGWARNING("%s: Trying to close an invalid LuaState, ignoring.", __FUNCTION__);
		return;
	}
	if (!m_IsOwned)
	{
		LOGWARNING(
			"%s: Detected mis-use, calling Close() on an attached state (0x%p). Detaching instead.",
			__FUNCTION__, static_cast<void *>(m_LuaState)
		);
		Detach();
		return;
	}

	// Invalidate all tracked refs:
	{
		cCSLock Lock(m_CSTrackedRefs);
		for (auto & r: m_TrackedRefs)
		{
			r->Invalidate();
		}
		m_TrackedRefs.clear();
	}

	cCanonLuaStates::Remove(*this);
	cLuaStateTracker::Del(*this);
	lua_close(m_LuaState);
	m_LuaState = nullptr;
	m_IsOwned = false;
}





void cLuaState::Attach(lua_State * a_State)
{
	if (m_LuaState != nullptr)
	{
		LOGINFO("%s: Already contains a LuaState (0x%p), will be closed / detached.", __FUNCTION__, static_cast<void *>(m_LuaState));
		if (m_IsOwned)
		{
			Close();
		}
		else
		{
			Detach();
		}
	}
	m_LuaState = a_State;
	m_IsOwned = false;
}





void cLuaState::Detach(void)
{
	if (m_LuaState == nullptr)
	{
		return;
	}
	if (m_IsOwned)
	{
		LOGWARNING(
			"%s: Detected a mis-use, calling Detach() when the state is owned. Closing the owned state (0x%p).",
			__FUNCTION__, static_cast<void *>(m_LuaState)
		);
		Close();
		return;
	}
	m_LuaState = nullptr;
}





void cLuaState::AddPackagePath(const AString & a_PathVariable, const AString & a_Path)
{
	ASSERT_LUA_STACK_BALANCE(m_LuaState);

	// Get the current path:
	lua_getfield(m_LuaState, LUA_GLOBALSINDEX, "package");                           // Stk: <package>
	lua_getfield(m_LuaState, -1, a_PathVariable.c_str());                            // Stk: <package> <package.path>
	size_t len = 0;
	const char * PackagePath = lua_tolstring(m_LuaState, -1, &len);

	// Append the new path:
	AString NewPackagePath(PackagePath, len);
	NewPackagePath.append(LUA_PATHSEP);
	NewPackagePath.append(a_Path);

	// Set the new path to the environment:
	lua_pop(m_LuaState, 1);                                                          // Stk: <package>
	lua_pushlstring(m_LuaState, NewPackagePath.c_str(), NewPackagePath.length());    // Stk: <package> <NewPackagePath>
	lua_setfield(m_LuaState, -2, a_PathVariable.c_str());                            // Stk: <package>
	lua_pop(m_LuaState, 1);                                                          // Stk: -
}





bool cLuaState::LoadFile(const AString & a_FileName, bool a_LogWarnings)
{
	ASSERT(IsValid());
	ASSERT_LUA_STACK_BALANCE(m_LuaState);

	// Load the file:
	int s = luaL_loadfile(m_LuaState, a_FileName.c_str());
	if (s != 0)
	{
		if (a_LogWarnings)
		{
			LOGWARNING("Can't load %s because of a load error in file %s: %d (%s)", m_SubsystemName.c_str(), a_FileName.c_str(), s, lua_tostring(m_LuaState, -1));
		}
		lua_pop(m_LuaState, 1);
		return false;
	}

	// Execute the globals:
	s = lua_pcall(m_LuaState, 0, 0, 0);
	if (s != 0)
	{
		if (a_LogWarnings)
		{
			LOGWARNING("Can't load %s because of an initialization error in file %s: %d (%s)", m_SubsystemName.c_str(), a_FileName.c_str(), s, lua_tostring(m_LuaState, -1));
		}
		lua_pop(m_LuaState, 1);
		return false;
	}

	return true;
}





bool cLuaState::LoadString(const AString & a_StringToLoad, const AString & a_FileName, bool a_LogWarnings)
{
	ASSERT(IsValid());
	ASSERT_LUA_STACK_BALANCE(m_LuaState);

	// Load the file:
	int s = luaL_loadstring(m_LuaState, a_StringToLoad.c_str());
	if (s != 0)
	{
		if (a_LogWarnings)
		{
			LOGWARNING("Can't load %s because of a load error in string from \"%s\": %d (%s)", m_SubsystemName.c_str(), a_FileName.c_str(), s, lua_tostring(m_LuaState, -1));
		}
		lua_pop(m_LuaState, 1);
		return false;
	}

	// Execute the globals:
	s = lua_pcall(m_LuaState, 0, LUA_MULTRET, 0);
	if (s != 0)
	{
		if (a_LogWarnings)
		{
			LOGWARNING("Can't load %s because of an initialization error in string from \"%s\": %d (%s)", m_SubsystemName.c_str(), a_FileName.c_str(), s, lua_tostring(m_LuaState, -1));
		}
		lua_pop(m_LuaState, 1);
		return false;
	}

	return true;
}





bool cLuaState::HasFunction(const char * a_FunctionName)
{
	if (!IsValid())
	{
		// This happens if cPlugin::Initialize() fails with an error
		return false;
	}

	ASSERT_LUA_STACK_BALANCE(m_LuaState);
	lua_getglobal(m_LuaState, a_FunctionName);
	bool res = (!lua_isnil(m_LuaState, -1) && lua_isfunction(m_LuaState, -1));
	lua_pop(m_LuaState, 1);
	return res;
}





bool cLuaState::PushFunction(const char * a_FunctionName)
{
	ASSERT(m_NumCurrentFunctionArgs == -1);  // If not, there's already something pushed onto the stack

	if (!IsValid())
	{
		// This happens if cPlugin::Initialize() fails with an error
		return false;
	}

	// Push the error handler for lua_pcall()
	lua_pushcfunction(m_LuaState, &ReportFnCallErrors);

	lua_getglobal(m_LuaState, a_FunctionName);
	if (!lua_isfunction(m_LuaState, -1))
	{
		LOGWARNING("Error in %s: Could not find function %s()", m_SubsystemName.c_str(), a_FunctionName);
		lua_pop(m_LuaState, 2);
		return false;
	}
	m_CurrentFunctionName.assign(a_FunctionName);
	m_NumCurrentFunctionArgs = 0;
	return true;
}





bool cLuaState::PushFunction(const cRef & a_FnRef)
{
	ASSERT(IsValid());
	ASSERT(m_NumCurrentFunctionArgs == -1);  // If not, there's already something pushed onto the stack

	// Push the error handler for lua_pcall()
	lua_pushcfunction(m_LuaState, &ReportFnCallErrors);

	lua_rawgeti(m_LuaState, LUA_REGISTRYINDEX, static_cast<int>(a_FnRef));  // same as lua_getref()
	if (!lua_isfunction(m_LuaState, -1))
	{
		lua_pop(m_LuaState, 2);
		return false;
	}
	m_CurrentFunctionName = "<callback>";
	m_NumCurrentFunctionArgs = 0;
	return true;
}





bool cLuaState::PushFunction(const cRef & a_TableRef, const char * a_FnName)
{
	ASSERT(IsValid());
	ASSERT(m_NumCurrentFunctionArgs == -1);  // If not, there's already something pushed onto the stack

	if (!a_TableRef.IsValid())
	{
		return false;
	}

	// Push the error handler for lua_pcall()
	lua_pushcfunction(m_LuaState, &ReportFnCallErrors);

	// Get the function from the table:
	lua_rawgeti(m_LuaState, LUA_REGISTRYINDEX, static_cast<int>(a_TableRef));
	if (!lua_istable(m_LuaState, -1))
	{
		// Not a table, bail out
		lua_pop(m_LuaState, 2);
		return false;
	}
	lua_getfield(m_LuaState, -1, a_FnName);
	if (lua_isnil(m_LuaState, -1) || !lua_isfunction(m_LuaState, -1))
	{
		// Not a valid function, bail out
		lua_pop(m_LuaState, 3);
		return false;
	}

	// Pop the table off the stack:
	lua_remove(m_LuaState, -2);

	m_CurrentFunctionName = fmt::format(FMT_STRING("<table-callback {}>"), a_FnName);
	m_NumCurrentFunctionArgs = 0;
	return true;
}





void cLuaState::Push(const AString & a_String)
{
	ASSERT(IsValid());

	lua_pushlstring(m_LuaState, a_String.data(), a_String.size());
}





void cLuaState::Push(const AStringMap & a_Dictionary)
{
	ASSERT(IsValid());

	lua_createtable(m_LuaState, 0, static_cast<int>(a_Dictionary.size()));
	int newTable = lua_gettop(m_LuaState);
	for (const auto & item: a_Dictionary)
	{
		Push(item.first);   // key
		Push(item.second);  // value
		lua_rawset(m_LuaState, newTable);
	}
}





void cLuaState::Push(const AStringVector & a_Vector)
{
	ASSERT(IsValid());

	lua_createtable(m_LuaState, static_cast<int>(a_Vector.size()), 0);
	int newTable = lua_gettop(m_LuaState);
	int index = 1;
	for (AStringVector::const_iterator itr = a_Vector.begin(), end = a_Vector.end(); itr != end; ++itr, ++index)
	{
		Push(*itr);
		lua_rawseti(m_LuaState, newTable, index);
	}
}





void cLuaState::Push(const char * a_Value)
{
	ASSERT(IsValid());

	tolua_pushstring(m_LuaState, a_Value);
}





void cLuaState::Push(const cItem & a_Item)
{
	ASSERT(IsValid());
	auto c = new cItem(a_Item);
	tolua_pushusertype_and_takeownership(m_LuaState, c, "cItem");
}





void cLuaState::Push(const cNil & a_Nil)
{
	ASSERT(IsValid());

	lua_pushnil(m_LuaState);
}





void cLuaState::Push(const cLuaState::cRef & a_Ref)
{
	ASSERT(IsValid());

	lua_rawgeti(m_LuaState, LUA_REGISTRYINDEX, static_cast<int>(a_Ref));
}





void cLuaState::Push(const ContiguousByteBufferView a_Data)
{
	ASSERT(IsValid());

	lua_pushlstring(m_LuaState, reinterpret_cast<const char *>(a_Data.data()), a_Data.size());
}





void cLuaState::Push(const Vector3d & a_Vector)
{
	ASSERT(IsValid());
	auto c = new Vector3d(a_Vector);
	tolua_pushusertype_and_takeownership(m_LuaState, c, "Vector3<double>");
}





void cLuaState::Push(const Vector3i & a_Vector)
{
	ASSERT(IsValid());
	auto c = new Vector3i(a_Vector);
	tolua_pushusertype_and_takeownership(m_LuaState, c, "Vector3<int>");
}





void cLuaState::Push(bool a_Value)
{
	ASSERT(IsValid());

	tolua_pushboolean(m_LuaState, a_Value ? 1 : 0);
}





void cLuaState::Push(const cEntity * a_Entity)
{
	// Once we can make Lua understand constness, this function shall receive a corresponding function body
	Push(const_cast<cEntity * >(a_Entity));
}





void cLuaState::Push(cEntity * a_Entity)
{
	ASSERT(IsValid());

	if (a_Entity == nullptr)
	{
		lua_pushnil(m_LuaState);
	}
	else
	{
		const char * ClassName = [&]
			{
				switch (a_Entity->GetEntityType())
				{
					case cEntity::etBoat:         return "cBoat";
					case cEntity::etExpOrb:       return "cExpOrb";
					case cEntity::etFallingBlock: return "cFallingBlock";
					case cEntity::etFloater:      return "cFloater";
					case cEntity::etItemFrame:    return "cItemFrame";
					case cEntity::etLeashKnot:    return "cLeashKnot";
					case cEntity::etPainting:     return "cPainting";
					case cEntity::etPickup:       return "cPickup";
					case cEntity::etPlayer:       return "cPlayer";
					case cEntity::etTNT:          return "cTNTEntity";

					case cEntity::etMonster:
					{
						// Don't push specific mob types, as those are not exported in the API:
						return "cMonster";
					}
					case cEntity::etProjectile:
					{
						// Push the specific projectile type:
						return a_Entity->GetClass();
					}

					case cEntity::etEntity:
					case cEntity::etEnderCrystal:
					case cEntity::etMinecart:
					{
						// Push the generic entity class type:
						return "cEntity";
					}
				}  // switch (EntityType)
				UNREACHABLE("Unsupported entity type");
			}();
		tolua_pushusertype(m_LuaState, a_Entity, ClassName);
	}
}





void cLuaState::Push(cLuaServerHandle * a_ServerHandle)
{
	ASSERT(IsValid());

	tolua_pushusertype(m_LuaState, a_ServerHandle, "cServerHandle");
}





void cLuaState::Push(cLuaTCPLink * a_TCPLink)
{
	ASSERT(IsValid());

	tolua_pushusertype(m_LuaState, a_TCPLink, "cTCPLink");
}





void cLuaState::Push(cLuaUDPEndpoint * a_UDPEndpoint)
{
	ASSERT(IsValid());

	tolua_pushusertype(m_LuaState, a_UDPEndpoint, "cUDPEndpoint");
}





void cLuaState::Push(double a_Value)
{
	ASSERT(IsValid());

	tolua_pushnumber(m_LuaState, a_Value);
}





void cLuaState::Push(int a_Value)
{
	ASSERT(IsValid());

	tolua_pushnumber(m_LuaState, a_Value);
}





void cLuaState::Push(long a_Value)
{
	ASSERT(IsValid());

	tolua_pushnumber(m_LuaState, static_cast<lua_Number>(a_Value));
}





void cLuaState::Push(UInt32 a_Value)
{
	ASSERT(IsValid());

	tolua_pushnumber(m_LuaState, a_Value);
}





void cLuaState::Push(std::chrono::milliseconds a_Value)
{
	ASSERT(IsValid());

	tolua_pushnumber(m_LuaState, static_cast<lua_Number>(a_Value.count()));
}





void cLuaState::Pop(int a_NumValuesToPop)
{
	ASSERT(IsValid());

	lua_pop(m_LuaState, a_NumValuesToPop);
}





bool cLuaState::GetStackValue(int a_StackPos, AString & a_Value)
{
	size_t len = 0;
	const char * data = lua_tolstring(m_LuaState, a_StackPos, &len);
	if (data != nullptr)
	{
		a_Value.assign(data, len);
		return true;
	}
	return false;
}





bool cLuaState::GetStackValue(int a_StackPos, AStringMap & a_Value)
{
	// Retrieve all values in a string => string dictionary table:
	if (!lua_istable(m_LuaState, a_StackPos))
	{
		return false;
	}
	cStackTable tbl(*this, a_StackPos);
	bool isValid = true;
	tbl.ForEachElement([&isValid, &a_Value](cLuaState & a_LuaState)
		{
			AString key, val;
			if (a_LuaState.GetStackValues(-2, key, val))
			{
				a_Value[key] = val;
			}
			else
			{
				isValid = false;
				return true;
			}
			return false;
		}
	);
	return isValid;
}





bool cLuaState::GetStackValue(int a_StackPos, AStringVector & a_Value)
{
	// Retrieve all values in an array of string table:
	if (!lua_istable(m_LuaState, a_StackPos))
	{
		return false;
	}
	cStackTable tbl(*this, a_StackPos);
	bool isValid = true;
	tbl.ForEachArrayElement([&](cLuaState & a_LuaState, int a_Index)
		{
			AString tempStr;
			if (a_LuaState.GetStackValue(-1, tempStr))
			{
				a_Value.push_back(std::move(tempStr));
			}
			else
			{
				isValid = false;
				return true;
			}
			return false;
		}
	);
	return isValid;
}





bool cLuaState::GetStackValue(int a_StackPos, bool & a_ReturnedVal)
{
	a_ReturnedVal = (tolua_toboolean(m_LuaState, a_StackPos, a_ReturnedVal ? 1 : 0) > 0);
	return true;
}





bool cLuaState::GetStackValue(int a_StackPos, cCallback & a_Callback)
{
	return a_Callback.RefStack(*this, a_StackPos);
}





bool cLuaState::GetStackValue(int a_StackPos, cCallbackPtr & a_Callback)
{
	if (a_Callback == nullptr)
	{
		a_Callback = std::make_unique<cCallback>();
	}
	return a_Callback->RefStack(*this, a_StackPos);
}





bool cLuaState::GetStackValue(int a_StackPos, cOptionalCallback & a_Callback)
{
	return a_Callback.RefStack(*this, a_StackPos);
}





bool cLuaState::GetStackValue(int a_StackPos, cOptionalCallbackPtr & a_Callback)
{
	if (a_Callback == nullptr)
	{
		a_Callback = std::make_unique<cOptionalCallback>();
	}
	return a_Callback->RefStack(*this, a_StackPos);
}





bool cLuaState::GetStackValue(int a_StackPos, cCallbackSharedPtr & a_Callback)
{
	if (a_Callback == nullptr)
	{
		a_Callback = std::make_shared<cCallback>();
	}
	return a_Callback->RefStack(*this, a_StackPos);
}





bool cLuaState::GetStackValue(int a_StackPos, cPluginManager::CommandResult & a_Result)
{
	if (lua_isnumber(m_LuaState, a_StackPos))
	{
		a_Result = static_cast<cPluginManager::CommandResult>(static_cast<int>((tolua_tonumber(m_LuaState, a_StackPos, a_Result))));
		return true;
	}
	return false;
}





bool cLuaState::GetStackValue(int a_StackPos, cRef & a_Ref)
{
	a_Ref.RefStack(*this, a_StackPos);
	return true;
}





bool cLuaState::GetStackValue(int a_StackPos, cStackTablePtr & a_StackTable)
{
	// Only allow tables to be stored in a_StackTable:
	if (!lua_istable(m_LuaState, a_StackPos))
	{
		return false;
	}

	// Assign the StackTable to the specified stack position:
	a_StackTable = std::make_unique<cStackTable>(*this, a_StackPos);
	return true;
}





bool cLuaState::GetStackValue(int a_StackPos, cTableRef & a_TableRef)
{
	return a_TableRef.RefStack(*this, a_StackPos);
}





bool cLuaState::GetStackValue(int a_StackPos, cTableRefPtr & a_TableRef)
{
	if (a_TableRef == nullptr)
	{
		a_TableRef = std::make_unique<cTableRef>();
	}
	return a_TableRef->RefStack(*this, a_StackPos);
}





bool cLuaState::GetStackValue(int a_StackPos, cTrackedRef & a_Ref)
{
	return a_Ref.RefStack(*this, a_StackPos);
}





bool cLuaState::GetStackValue(int a_StackPos, cTrackedRefPtr & a_Ref)
{
	if (a_Ref == nullptr)
	{
		a_Ref = std::make_unique<cTrackedRef>();
	}
	return a_Ref->RefStack(*this, a_StackPos);
}





bool cLuaState::GetStackValue(int a_StackPos, cTrackedRefSharedPtr & a_Ref)
{
	if (a_Ref == nullptr)
	{
		a_Ref = std::make_shared<cTrackedRef>();
	}
	return a_Ref->RefStack(*this, a_StackPos);
}





bool cLuaState::GetStackValue(int a_StackPos, ContiguousByteBuffer & a_Data)
{
	size_t Length = 0;
	const char * const Data = lua_tolstring(m_LuaState, a_StackPos, &Length);
	if (Data != nullptr)
	{
		a_Data.assign(reinterpret_cast<const std::byte *>(Data), Length);
		return true;
	}
	return false;
}





bool cLuaState::GetStackValue(int a_StackPos, CustomStatistic & a_Value)
{
	if (lua_isnumber(m_LuaState, a_StackPos))
	{
		a_Value = static_cast<CustomStatistic>(static_cast<std::underlying_type_t<CustomStatistic>>(lua_tonumber(m_LuaState, a_StackPos)));
		return true;
	}

	return true;
}





bool cLuaState::GetStackValue(int a_StackPos, double & a_ReturnedVal)
{
	if (lua_isnumber(m_LuaState, a_StackPos))
	{
		a_ReturnedVal = tolua_tonumber(m_LuaState, a_StackPos, a_ReturnedVal);
		return true;
	}
	return false;
}





bool cLuaState::GetStackValue(int a_StackPos, eBlockFace & a_ReturnedVal)
{
	if (!lua_isnumber(m_LuaState, a_StackPos))
	{
		return false;
	}
	a_ReturnedVal = static_cast<eBlockFace>(Clamp(
		static_cast<int>(tolua_tonumber(m_LuaState, a_StackPos, a_ReturnedVal)),
		static_cast<int>(BLOCK_FACE_MIN), static_cast<int>(BLOCK_FACE_MAX))
	);
	return true;
}





bool cLuaState::GetStackValue(int a_StackPos, eWeather & a_ReturnedVal)
{
	if (!lua_isnumber(m_LuaState, a_StackPos))
	{
		return false;
	}
	a_ReturnedVal = static_cast<eWeather>(Clamp(
		static_cast<int>(tolua_tonumber(m_LuaState, a_StackPos, a_ReturnedVal)),
		static_cast<int>(wSunny), static_cast<int>(wThunderstorm))
	);
	return true;
}





bool cLuaState::GetStackValue(int a_StackPos, float & a_ReturnedVal)
{
	if (lua_isnumber(m_LuaState, a_StackPos))
	{
		a_ReturnedVal = static_cast<float>(tolua_tonumber(m_LuaState, a_StackPos, a_ReturnedVal));
		return true;
	}
	return false;
}





bool cLuaState::GetStackValue(int a_StackPos, cUUID & a_Value)
{
	if (lua_isnil(m_LuaState, a_StackPos))
	{
		return false;
	}

	tolua_Error tolua_Err;
	if (tolua_isusertype(m_LuaState, a_StackPos, "cUUID", 0, &tolua_Err))
	{
		// Found a cUUID, copy into output value
		cUUID * PtrUUID = nullptr;
		GetStackValue(a_StackPos, PtrUUID);
		if (PtrUUID == nullptr)
		{
			return false;
		}
		a_Value = *PtrUUID;
		return true;
	}

	// Try to get a string and parse as a UUID into the output
	AString StrUUID;
	if (!GetStackValue(a_StackPos, StrUUID))
	{
		return false;
	}
	return a_Value.FromString(StrUUID);
}





bool cLuaState::GetStackValue(int a_StackPos, std::string_view & a_Value)
{
	size_t Length = 0;
	const char * const Value = lua_tolstring(m_LuaState, a_StackPos, &Length);
	if (Value != nullptr)
	{
		a_Value = { Value, Length };
		return true;
	}
	return false;
}





template <typename T>
bool cLuaState::GetStackValue(int a_StackPos, Vector3<T> & a_ReturnedVal)
{
	tolua_Error err;
	if (lua_isnil(m_LuaState, a_StackPos))
	{
		return false;
	}
	if (tolua_isusertype(m_LuaState, a_StackPos, "Vector3<double>", 0, &err))
	{
		a_ReturnedVal = **(static_cast<const Vector3d **>(lua_touserdata(m_LuaState, a_StackPos)));
		return true;
	}
	if (tolua_isusertype(m_LuaState, a_StackPos, "Vector3<float>", 0, &err))
	{
		a_ReturnedVal = **(static_cast<const Vector3f **>(lua_touserdata(m_LuaState, a_StackPos)));
		return true;
	}
	if (tolua_isusertype(m_LuaState, a_StackPos, "Vector3<int>", 0, &err))
	{
		a_ReturnedVal = **(static_cast<const Vector3i **>(lua_touserdata(m_LuaState, a_StackPos)));
		return true;
	}

	// Bonus: Allow simple tables to work as Vector3:
	if (lua_istable(m_LuaState, a_StackPos))
	{
		lua_rawgeti(m_LuaState, a_StackPos, 1);
		lua_rawgeti(m_LuaState, a_StackPos, 2);
		lua_rawgeti(m_LuaState, a_StackPos, 3);
		T x, y, z;
		if (!GetStackValues(-3, x, y, z))
		{
			return false;
		}
		a_ReturnedVal = Vector3<T>(x, y, z);
		return true;
	}

	return false;
}

// Explicitly instantiate the previous function for all Vector3 types:
template bool cLuaState::GetStackValue(int a_StackPos, Vector3d & a_ReturnedVal);
template bool cLuaState::GetStackValue(int a_StackPos, Vector3f & a_ReturnedVal);
template bool cLuaState::GetStackValue(int a_StackPos, Vector3i & a_ReturnedVal);





cLuaState::cStackValue cLuaState::WalkToValue(const AString & a_Name)
{
	// There needs to be at least one value on the stack:
	ASSERT(lua_gettop(m_LuaState) > 0);

	// Iterate over path and replace the top of the stack with the walked element
	lua_pushvalue(m_LuaState, -1);  // Copy the stack value into the "working area"
	auto path = StringSplit(a_Name, ".");
	for (const auto & elem: path)
	{
		// If the value is not a table, bail out (error):
		if (!lua_istable(m_LuaState, -1))
		{
			lua_pop(m_LuaState, 1);
			return cStackValue();
		}

		// Get the next part of the path:
		lua_getfield(m_LuaState, -1, elem.c_str());

		// Remove the previous value from the stack (keep only the new one):
		lua_remove(m_LuaState, -2);
	}  // for elem - path[]
	if (lua_isnil(m_LuaState, -1))
	{
		lua_pop(m_LuaState, 1);
		return cStackValue();
	}
	return cStackValue(*this);
}





cLuaState::cStackValue cLuaState::WalkToNamedGlobal(const AString & a_Name)
{
	// Iterate over path and replace the top of the stack with the walked element
	lua_getglobal(m_LuaState, "_G");
	auto path = StringSplit(a_Name, ".");
	for (const auto & elem: path)
	{
		// If the value is not a table, bail out (error):
		if (!lua_istable(m_LuaState, -1))
		{
			lua_pop(m_LuaState, 1);
			return cStackValue();
		}

		// Get the next part of the path:
		lua_getfield(m_LuaState, -1, elem.c_str());

		// Remove the previous value from the stack (keep only the new one):
		lua_remove(m_LuaState, -2);
	}  // for elem - path[]
	if (lua_isnil(m_LuaState, -1))
	{
		lua_pop(m_LuaState, 1);
		return cStackValue();
	}
	return cStackValue(*this);
}





bool cLuaState::CallFunction(int a_NumResults)
{
	ASSERT (m_NumCurrentFunctionArgs >= 0);  // A function must be pushed to stack first
	ASSERT(lua_isfunction(m_LuaState, -m_NumCurrentFunctionArgs - 1));  // The function to call
	ASSERT(lua_isfunction(m_LuaState, -m_NumCurrentFunctionArgs - 2));  // The error handler

	// Save the current "stack" state and reset, in case the callback calls another function:
	AString CurrentFunctionName;
	std::swap(m_CurrentFunctionName, CurrentFunctionName);
	int NumArgs = m_NumCurrentFunctionArgs;
	m_NumCurrentFunctionArgs = -1;

	// Call the function:
	int s = lua_pcall(m_LuaState, NumArgs, a_NumResults, -NumArgs - 2);
	if (s != 0)
	{
		// The error has already been printed together with the stacktrace
		LOGWARNING("Error in %s calling function %s()", m_SubsystemName, CurrentFunctionName);

		// Remove the error handler and error message from the stack:
		auto top = lua_gettop(m_LuaState);
		if (top < 2)
		{
			LogStackValues(fmt::format(FMT_STRING("The Lua stack is in an unexpected state, expected at least two values there, but got {}"), top).c_str());
		}
		lua_pop(m_LuaState, std::min(2, top));
		return false;
	}

	// Remove the error handler from the stack:
	lua_remove(m_LuaState, -a_NumResults - 1);
	return true;
}





bool cLuaState::CheckParamUserTable(int a_StartParam, const char * a_UserTable, int a_EndParam)
{
	ASSERT(IsValid());

	if (a_EndParam < 0)
	{
		a_EndParam = a_StartParam;
	}

	tolua_Error tolua_err;
	for (int i = a_StartParam; i <= a_EndParam; i++)
	{
		if (tolua_isusertable(m_LuaState, i, a_UserTable, 0, &tolua_err))
		{
			continue;
		}
		// Not the correct parameter
		lua_Debug entry;
		VERIFY(lua_getstack(m_LuaState, 0,   &entry));
		VERIFY(lua_getinfo (m_LuaState, "n", &entry));
		AString ErrMsg = fmt::format(FMT_STRING("#ferror in function '{}'."), (entry.name != nullptr) ? entry.name : "?");
		tolua_error(m_LuaState, ErrMsg.c_str(), &tolua_err);
		return false;
	}  // for i - Param

	// All params checked ok
	return true;
}





bool cLuaState::CheckParamUserType(int a_StartParam, const char * a_UserType, int a_EndParam)
{
	ASSERT(IsValid());

	if (a_EndParam < 0)
	{
		a_EndParam = a_StartParam;
	}

	tolua_Error tolua_err;
	for (int i = a_StartParam; i <= a_EndParam; i++)
	{
		if (tolua_isusertype(m_LuaState, i, a_UserType, 0, &tolua_err))
		{
			continue;
		}
		// Not the correct parameter
		lua_Debug entry;
		VERIFY(lua_getstack(m_LuaState, 0,   &entry));
		VERIFY(lua_getinfo (m_LuaState, "n", &entry));
		AString ErrMsg = fmt::format(FMT_STRING("#ferror in function '{}'."), (entry.name != nullptr) ? entry.name : "?");
		tolua_error(m_LuaState, ErrMsg.c_str(), &tolua_err);
		return false;
	}  // for i - Param

	// All params checked ok
	return true;
}





bool cLuaState::CheckParamTable(int a_StartParam, int a_EndParam)
{
	ASSERT(IsValid());

	if (a_EndParam < 0)
	{
		a_EndParam = a_StartParam;
	}

	tolua_Error tolua_err;
	for (int i = a_StartParam; i <= a_EndParam; i++)
	{
		if (tolua_istable(m_LuaState, i, 0, &tolua_err))
		{
			continue;
		}
		// Not the correct parameter
		lua_Debug entry;
		VERIFY(lua_getstack(m_LuaState, 0,   &entry));
		VERIFY(lua_getinfo (m_LuaState, "n", &entry));
		AString ErrMsg = fmt::format(FMT_STRING("#ferror in function '{}'."), (entry.name != nullptr) ? entry.name : "?");

		BreakIntoDebugger(m_LuaState);

		tolua_error(m_LuaState, ErrMsg.c_str(), &tolua_err);
		return false;
	}  // for i - Param

	// All params checked ok
	return true;
}





bool cLuaState::CheckParamNumber(int a_StartParam, int a_EndParam)
{
	ASSERT(IsValid());

	if (a_EndParam < 0)
	{
		a_EndParam = a_StartParam;
	}

	tolua_Error tolua_err;
	for (int i = a_StartParam; i <= a_EndParam; i++)
	{
		if (tolua_isnumber(m_LuaState, i, 0, &tolua_err))
		{
			continue;
		}
		// Not the correct parameter
		lua_Debug entry;
		VERIFY(lua_getstack(m_LuaState, 0,   &entry));
		VERIFY(lua_getinfo (m_LuaState, "n", &entry));
		AString ErrMsg = fmt::format(FMT_STRING("#ferror in function '{}'."), (entry.name != nullptr) ? entry.name : "?");
		tolua_error(m_LuaState, ErrMsg.c_str(), &tolua_err);
		return false;
	}  // for i - Param

	// All params checked ok
	return true;
}





bool cLuaState::CheckParamBool(int a_StartParam, int a_EndParam)
{
	ASSERT(IsValid());

	if (a_EndParam < 0)
	{
		a_EndParam = a_StartParam;
	}

	tolua_Error tolua_err;
	for (int i = a_StartParam; i <= a_EndParam; i++)
	{
		if (tolua_isboolean(m_LuaState, i, 0, &tolua_err))
		{
			continue;
		}
		// Not the correct parameter
		lua_Debug entry;
		VERIFY(lua_getstack(m_LuaState, 0,   &entry));
		VERIFY(lua_getinfo (m_LuaState, "n", &entry));
		AString ErrMsg = fmt::format(FMT_STRING("#ferror in function '{}'."), (entry.name != nullptr) ? entry.name : "?");
		tolua_error(m_LuaState, ErrMsg.c_str(), &tolua_err);
		return false;
	}  // for i - Param

	// All params checked ok
	return true;
}





bool cLuaState::CheckParamString(int a_StartParam, int a_EndParam)
{
	ASSERT(IsValid());

	if (a_EndParam < 0)
	{
		a_EndParam = a_StartParam;
	}

	tolua_Error tolua_err;
	for (int i = a_StartParam; i <= a_EndParam; i++)
	{
		if (lua_isstring(m_LuaState, i))
		{
			continue;
		}
		// Not the correct parameter
		lua_Debug entry;
		VERIFY(lua_getstack(m_LuaState, 0,   &entry));
		VERIFY(lua_getinfo (m_LuaState, "n", &entry));
		tolua_err.array = 0;
		tolua_err.type = "string";
		tolua_err.index = i;
		AString ErrMsg = fmt::format(FMT_STRING("#ferror in function '{}'."), (entry.name != nullptr) ? entry.name : "?");
		tolua_error(m_LuaState, ErrMsg.c_str(), &tolua_err);
		return false;
	}  // for i - Param

	// All params checked ok
	return true;
}





bool cLuaState::CheckParamFunction(int a_StartParam, int a_EndParam)
{
	ASSERT(IsValid());

	if (a_EndParam < 0)
	{
		a_EndParam = a_StartParam;
	}

	for (int i = a_StartParam; i <= a_EndParam; i++)
	{
		if (lua_isfunction(m_LuaState, i))
		{
			continue;
		}
		// Not the correct parameter
		lua_Debug entry;
		VERIFY(lua_getstack(m_LuaState, 0,   &entry));
		VERIFY(lua_getinfo (m_LuaState, "n", &entry));
		luaL_error(m_LuaState, "Error in function '%s' parameter #%d. Function expected, got %s",
			(entry.name != nullptr) ? entry.name : "?", i, GetTypeText(i).c_str()
		);
		return false;
	}  // for i - Param

	// All params checked ok
	return true;
}





bool cLuaState::CheckParamFunctionOrNil(int a_StartParam, int a_EndParam)
{
	ASSERT(IsValid());

	if (a_EndParam < 0)
	{
		a_EndParam = a_StartParam;
	}

	for (int i = a_StartParam; i <= a_EndParam; i++)
	{
		if (lua_isfunction(m_LuaState, i) || lua_isnil(m_LuaState, i))
		{
			continue;
		}
		// Not the correct parameter
		lua_Debug entry;
		VERIFY(lua_getstack(m_LuaState, 0,   &entry));
		VERIFY(lua_getinfo (m_LuaState, "n", &entry));
		luaL_error(m_LuaState, "Error in function '%s' parameter #%d. Function expected, got %s",
			(entry.name != nullptr) ? entry.name : "?", i, GetTypeText(i).c_str()
		);
		return false;
	}  // for i - Param

	// All params checked ok
	return true;
}





bool cLuaState::CheckParamVector3(int a_StartParam, int a_EndParam)
{
	ASSERT(IsValid());

	if (a_EndParam < 0)
	{
		a_EndParam = a_StartParam;
	}

	for (int i = a_StartParam; i <= a_EndParam; ++i)
	{
		if (IsParamVector3(a_StartParam))
		{
			continue;
		}

		ApiParamError(fmt::format(FMT_STRING("Failed to read parameter #{}. Vector3 expected, got {}"), i, GetTypeText(i)));
		return false;
	}
	return true;
}





bool cLuaState::CheckParamUUID(int a_StartParam, int a_EndParam)
{
	ASSERT(IsValid());

	if (a_EndParam < 0)
	{
		a_EndParam = a_StartParam;
	}

	cUUID tempUUID;
	AString tempStr;
	// Accept either a cUUID or a string that contains a valid UUID
	for (int i = a_StartParam; i <= a_EndParam; ++i)
	{
		tolua_Error err;
		if (tolua_isusertype(m_LuaState, i, "cUUID", 0, &err) && !lua_isnil(m_LuaState, i))
		{
			continue;
		}

		if (!tolua_iscppstring(m_LuaState, i, 0, &err))
		{
			ApiParamError(fmt::format(FMT_STRING("Failed to read parameter #{}. UUID expected, got {}"), i, GetTypeText(i)));
			return false;
		}

		// Check string is a valid UUID
		GetStackValue(i, tempStr);
		if (!tempUUID.FromString(tempStr))
		{
			ApiParamError(fmt::format(FMT_STRING("Failed to read parameter #{}. UUID expected, got non-UUID string:\n\t\"{}\""), i, tempStr));
			return false;
		}
	}
	return true;
}





bool cLuaState::CheckParamEnd(int a_Param)
{
	tolua_Error tolua_err;
	if (tolua_isnoobj(m_LuaState, a_Param, &tolua_err) == 1)
	{
		return true;
	}
	// Not the correct parameter
	lua_Debug entry;
	VERIFY(lua_getstack(m_LuaState, 0,   &entry));
	VERIFY(lua_getinfo (m_LuaState, "n", &entry));
	AString ErrMsg = fmt::format(FMT_STRING("#ferror in function '{}': Too many arguments."), (entry.name != nullptr) ? entry.name : "?");
	tolua_error(m_LuaState, ErrMsg.c_str(), &tolua_err);
	return false;
}





bool cLuaState::CheckParamSelf(const char * a_SelfClassName)
{
	tolua_Error tolua_err;
	if (tolua_isusertype(m_LuaState, 1, a_SelfClassName, 0, &tolua_err) && !lua_isnil(m_LuaState, 1))
	{
		return true;
	}

	// Not the correct parameter
	lua_Debug entry;
	VERIFY(lua_getstack(m_LuaState, 0,   &entry));
	VERIFY(lua_getinfo (m_LuaState, "n", &entry));
	AString ErrMsg = fmt::format(
		FMT_STRING("Error in function '{}'. The 'self' parameter is not of the expected type, \"instance of {}\". " \
		"Make sure you're using the correct calling convention (obj:fn() instead of obj.fn())."),
		(entry.name != nullptr) ? entry.name : "<unknown>", a_SelfClassName
	);
	tolua_error(m_LuaState, ErrMsg.c_str(), &tolua_err);
	return false;
}





bool cLuaState::CheckParamStaticSelf(const char * a_SelfClassName)
{
	tolua_Error tolua_err;
	if (tolua_isusertable(m_LuaState, 1, a_SelfClassName, 0, &tolua_err) && !lua_isnil(m_LuaState, 1))
	{
		return true;
	}

	// Not the correct parameter
	lua_Debug entry;
	VERIFY(lua_getstack(m_LuaState, 0,   &entry));
	VERIFY(lua_getinfo (m_LuaState, "n", &entry));
	AString ErrMsg = fmt::format(
		FMT_STRING("Error in function '{}'. The 'self' parameter is not of the expected type, \"class {}\". " \
		"Make sure you're using the correct calling convention (cClassName:fn() instead of cClassName.fn() or obj:fn())."),
		(entry.name != nullptr) ? entry.name : "<unknown>", a_SelfClassName
	);
	tolua_error(m_LuaState, ErrMsg.c_str(), &tolua_err);
	return false;
}





bool cLuaState::IsParamUserType(int a_ParamIdx, const AString & a_UserType)
{
	ASSERT(IsValid());

	tolua_Error tolua_err;
	return (tolua_isusertype(m_LuaState, a_ParamIdx, a_UserType.c_str(), 0, &tolua_err) == 1);
}





bool cLuaState::IsParamNumber(int a_ParamIdx)
{
	ASSERT(IsValid());

	tolua_Error tolua_err;
	return (tolua_isnumber(m_LuaState, a_ParamIdx, 0, &tolua_err) == 1);
}





bool cLuaState::IsParamVector3(int a_ParamIdx)
{
	ASSERT(IsValid());

	return (
		IsParamUserType(a_ParamIdx, "Vector3<double>") ||
		IsParamUserType(a_ParamIdx, "Vector3<float>") ||
		IsParamUserType(a_ParamIdx, "Vector3<int>") ||
		lua_istable(m_LuaState, a_ParamIdx)  // Assume any table is good enough
	);
}





bool cLuaState::ReportErrors(int a_Status)
{
	return ReportErrors(m_LuaState, a_Status);
}





bool cLuaState::ReportErrors(lua_State * a_LuaState, int a_Status)
{
	if (a_Status == 0)
	{
		// No error to report
		return false;
	}

	LOGWARNING("LUA: %d - %s", a_Status, lua_tostring(a_LuaState, -1));
	lua_pop(a_LuaState, 1);
	return true;
}





void cLuaState::LogStackTrace(int a_StartingDepth)
{
	LogStackTrace(m_LuaState, a_StartingDepth);
}





void cLuaState::LogStackTrace(lua_State * a_LuaState, int a_StartingDepth)
{
	LOGWARNING("Stack trace:");
	lua_Debug entry;
	int depth = a_StartingDepth;
	while (lua_getstack(a_LuaState, depth, &entry))
	{
		lua_getinfo(a_LuaState, "Sln", &entry);
		LOGWARNING("  %s(%d): %s", entry.short_src, entry.currentline, entry.name ? entry.name : "(no name)");
		depth++;
	}
	LOGWARNING("Stack trace end");
}





int cLuaState::ApiParamError(std::string_view a_Msg)
{
	// Retrieve current function name
	lua_Debug entry;
	VERIFY(lua_getstack(m_LuaState, 0, &entry));
	VERIFY(lua_getinfo(m_LuaState, "n", &entry));

	// Compose the error message:
	AString errorMsg = fmt::format("{0}: {1}", (entry.name != nullptr) ? entry.name : "<unknown function>", a_Msg);

	// Log everything into the console:
	LOGWARNING("%s", errorMsg.c_str());
	// cLuaState::LogStackTrace(a_LuaState);  // Do NOT log stack trace, it is already output as part of the Lua error handling
	LogStackValues(m_LuaState, "Parameters on the stack");

	// Raise Lua error:
	lua_pushstring(m_LuaState, errorMsg.c_str());
	return lua_error(m_LuaState);
}





AString cLuaState::GetTypeText(int a_StackPos)
{
	return lua_typename(m_LuaState, lua_type(m_LuaState, a_StackPos));
}





int cLuaState::CallFunctionWithForeignParams(
	const AString & a_FunctionName,
	cLuaState & a_SrcLuaState,
	int a_SrcParamStart,
	int a_SrcParamEnd
)
{
	ASSERT(IsValid());
	ASSERT(a_SrcLuaState.IsValid());

	// Store the stack position before any changes
	int OldTop = lua_gettop(m_LuaState);

	// Push the function to call, including the error handler:
	if (!PushFunction(a_FunctionName.c_str()))
	{
		LOGWARNING("Function '%s' not found", a_FunctionName.c_str());
		lua_settop(m_LuaState, OldTop);
		return -1;
	}

	// Copy the function parameters to the target state
	if (CopyStackFrom(a_SrcLuaState, a_SrcParamStart, a_SrcParamEnd) < 0)
	{
		// Something went wrong, fix the stack and exit
		lua_settop(m_LuaState, OldTop);
		m_NumCurrentFunctionArgs = -1;
		m_CurrentFunctionName.clear();
		return -1;
	}

	// Call the function, with an error handler:
	int s = lua_pcall(m_LuaState, a_SrcParamEnd - a_SrcParamStart + 1, LUA_MULTRET, OldTop + 1);
	if (ReportErrors(s))
	{
		LOGWARN("Error while calling function '%s' in '%s'", a_FunctionName.c_str(), m_SubsystemName.c_str());
		// Reset the stack:
		lua_settop(m_LuaState, OldTop);

		// Reset the internal checking mechanisms:
		m_NumCurrentFunctionArgs = -1;
		m_CurrentFunctionName.clear();

		// Make Lua think everything is okay and return 0 values, so that plugins continue executing.
		// The failure is indicated by the zero return values.
		return 0;
	}

	// Reset the internal checking mechanisms:
	m_NumCurrentFunctionArgs = -1;
	m_CurrentFunctionName.clear();

	// Remove the error handler from the stack:
	lua_remove(m_LuaState, OldTop + 1);

	// Return the number of return values:
	return lua_gettop(m_LuaState) - OldTop;
}





int cLuaState::CopyStackFrom(cLuaState & a_SrcLuaState, int a_SrcStart, int a_SrcEnd, int a_NumAllowedNestingLevels)
{
	/*
	// DEBUG:
	LOGD("Copying stack values from %d to %d", a_SrcStart, a_SrcEnd);
	a_SrcLuaState.LogStack("Src stack before copying:");
	LogStack("Dst stack before copying:");
	*/
	for (int i = a_SrcStart; i <= a_SrcEnd; ++i)
	{
		if (!CopySingleValueFrom(a_SrcLuaState, i, a_NumAllowedNestingLevels))
		{
			lua_pop(m_LuaState, i - a_SrcStart);
			return -1;
		}
	}
	return a_SrcEnd - a_SrcStart + 1;
}





bool cLuaState::CopyTableFrom(cLuaState & a_SrcLuaState, int a_SrcStackIdx, int a_NumAllowedNestingLevels)
{
	[[maybe_unused]] const auto srcTop = lua_gettop(a_SrcLuaState);
	[[maybe_unused]] const auto dstTop = lua_gettop(m_LuaState);

	// Create the dest table:
	lua_createtable(m_LuaState, 0, 0);            // DST: <table>
	lua_pushvalue(a_SrcLuaState, a_SrcStackIdx);  // SRC: <table>
	lua_pushnil(a_SrcLuaState);                   // SRC: <table> <key>
	while (lua_next(a_SrcLuaState, -2) != 0)      // SRC: <table> <key> <value>
	{
		ASSERT(lua_gettop(a_SrcLuaState) == srcTop + 3);
		ASSERT(lua_gettop(m_LuaState) == dstTop + 1);

		// Copy the key:
		if (!CopySingleValueFrom(a_SrcLuaState, -2, a_NumAllowedNestingLevels))  // DST: <table> <key>
		{
			lua_pop(m_LuaState, 1);
			lua_pop(a_SrcLuaState, 3);
			ASSERT(lua_gettop(a_SrcLuaState) == srcTop);
			ASSERT(lua_gettop(m_LuaState) == dstTop);
			return false;
		}
		ASSERT(lua_gettop(a_SrcLuaState) == srcTop + 3);
		ASSERT(lua_gettop(m_LuaState) == dstTop + 2);

		// Copy the value:
		if (!CopySingleValueFrom(a_SrcLuaState, -1, a_NumAllowedNestingLevels - 1))  // DST: <table> <key> <value>
		{
			lua_pop(m_LuaState, 2);     // DST: empty
			lua_pop(a_SrcLuaState, 3);  // SRC: empty
			ASSERT(lua_gettop(a_SrcLuaState) == srcTop);
			ASSERT(lua_gettop(m_LuaState) == dstTop);
			return false;
		}
		ASSERT(lua_gettop(a_SrcLuaState) == srcTop + 3);
		ASSERT(lua_gettop(m_LuaState) == dstTop + 3);

		// Set the value and fix up stacks:
		lua_rawset(m_LuaState, -3);  // DST: <table>
		lua_pop(a_SrcLuaState, 1);  // SRC: <table> <key>
		ASSERT(lua_gettop(a_SrcLuaState) == srcTop + 2);
		ASSERT(lua_gettop(m_LuaState) == dstTop + 1);
	}
	lua_pop(a_SrcLuaState, 1);  // SRC: empty
	ASSERT(lua_gettop(a_SrcLuaState) == srcTop);
	ASSERT(lua_gettop(m_LuaState) == dstTop + 1);
	return true;
}





bool cLuaState::CopySingleValueFrom(cLuaState & a_SrcLuaState, int a_StackIdx, int a_NumAllowedNestingLevels)
{
	int t = lua_type(a_SrcLuaState, a_StackIdx);
	switch (t)
	{
		case LUA_TNIL:
		{
			lua_pushnil(m_LuaState);
			return true;
		}
		case LUA_TSTRING:
		{
			AString s;
			a_SrcLuaState.ToString(a_StackIdx, s);
			Push(s);
			return true;
		}
		case LUA_TBOOLEAN:
		{
			bool b = (tolua_toboolean(a_SrcLuaState, a_StackIdx, false) != 0);
			Push(b);
			return true;
		}
		case LUA_TNUMBER:
		{
			lua_Number d = tolua_tonumber(a_SrcLuaState, a_StackIdx, 0);
			Push(d);
			return true;
		}
		case LUA_TUSERDATA:
		{
			// Get the class name:
			const char * type = nullptr;
			if (lua_getmetatable(a_SrcLuaState, a_StackIdx) == 0)
			{
				LOGWARNING("%s: Unknown class in pos %d, cannot copy.", __FUNCTION__, a_StackIdx);
				return false;
			}
			lua_rawget(a_SrcLuaState, LUA_REGISTRYINDEX);  // Stack +1
			type = lua_tostring(a_SrcLuaState, -1);
			lua_pop(a_SrcLuaState, 1);                     // Stack -1

			// Copy the value:
			void * ud = tolua_touserdata(a_SrcLuaState, a_StackIdx, nullptr);
			tolua_pushusertype(m_LuaState, ud, type);
			return true;
		}
		case LUA_TTABLE:
		{
			if (!CopyTableFrom(a_SrcLuaState, a_StackIdx, a_NumAllowedNestingLevels - 1))
			{
				LOGWARNING("%s: Failed to copy table in pos %d.", __FUNCTION__, a_StackIdx);
				return false;
			}
			return true;
		}
		default:
		{
			LOGWARNING("%s: Unsupported value: '%s' at stack position %d. Can only copy numbers, strings, bools, classes and simple tables!",
				__FUNCTION__, lua_typename(a_SrcLuaState, t), a_StackIdx
			);
			return false;
		}
	}
}





void cLuaState::ToString(int a_StackPos, AString & a_String)
{
	size_t len;
	const char * s = lua_tolstring(m_LuaState, a_StackPos, &len);
	if (s != nullptr)
	{
		a_String.assign(s, len);
	}
}





void cLuaState::LogStackValues(const char * a_Header)
{
	LogStackValues(m_LuaState, a_Header);
}





void cLuaState::LogStackValues(lua_State * a_LuaState, const char * a_Header)
{
	// Format string consisting only of %s is used to appease the compiler
	ASSERT_LUA_STACK_BALANCE(a_LuaState, false);
	LOG("%s", (a_Header != nullptr) ? a_Header : "Lua C API Stack contents:");
	for (int i = lua_gettop(a_LuaState); i > 0; i--)
	{
		AString Value;
		int Type = lua_type(a_LuaState, i);
		switch (Type)
		{
			case LUA_TBOOLEAN: Value.assign((lua_toboolean(a_LuaState, i) != 0) ? "true" : "false"); break;
			case LUA_TLIGHTUSERDATA: Value = fmt::format(FMT_STRING("{}"), lua_touserdata(a_LuaState, i)); break;
			case LUA_TNUMBER:        Value = fmt::format(FMT_STRING("{}"), lua_tonumber(a_LuaState, i)); break;
			case LUA_TSTRING:
			{
				size_t len;
				const char * txt = lua_tolstring(a_LuaState, i, &len);
				Value.assign(txt, std::min<size_t>(len, 50));  // Only log up to 50 characters of the string
				break;
			}
			case LUA_TTABLE:         Value = fmt::format(FMT_STRING("{}"), lua_topointer(a_LuaState, i)); break;
			case LUA_TFUNCTION:      Value = fmt::format(FMT_STRING("{}"), lua_topointer(a_LuaState, i)); break;
			case LUA_TUSERDATA:
			{
				Value = fmt::format(FMT_STRING("{} ({})"), lua_touserdata(a_LuaState, i), tolua_typename(a_LuaState, i));
				// tolua_typename pushes the string onto Lua stack, pop it off again:
				lua_pop(a_LuaState, 1);
				break;
			}
			default: break;
		}
		LOGD("  Idx %d: type %d (%s) %s", i, Type, lua_typename(a_LuaState, Type), Value.c_str());
	}  // for i - stack idx
}





cLuaState * cLuaState::QueryCanonLuaState(void) const
{
	return cCanonLuaStates::GetCanonState(m_LuaState);
}





void cLuaState::LogApiCallParamFailure(const char * a_FnName, const char * a_ParamNames)
{
	LOGWARNING("%s: Cannot read params: %s, bailing out.", a_FnName, a_ParamNames);
	LogStackTrace();
	LogStackValues("Values on the stack");
}





void cLuaState::TrackInDeadlockDetect(cDeadlockDetect & a_DeadlockDetect)
{
	a_DeadlockDetect.TrackCriticalSection(m_CS, fmt::format(FMT_STRING("cLuaState {}"), m_SubsystemName));
}





void cLuaState::UntrackInDeadlockDetect(cDeadlockDetect & a_DeadlockDetect)
{
	a_DeadlockDetect.UntrackCriticalSection(m_CS);
}





int cLuaState::ReportFnCallErrors(lua_State * a_LuaState)
{
	LOGWARNING("LUA: %s", lua_tostring(a_LuaState, -1));
	LogStackTrace(a_LuaState, 1);
	BreakIntoDebugger(a_LuaState);
	return 1;  // We left the error message on the stack as the return value
}





int cLuaState::BreakIntoDebugger(lua_State * a_LuaState)
{
	ASSERT_LUA_STACK_BALANCE(a_LuaState);
	// Call the BreakIntoDebugger function, if available:
	lua_getglobal(a_LuaState, "BreakIntoDebugger");
	if (!lua_isfunction(a_LuaState, -1))
	{
		LOGD("LUA: BreakIntoDebugger() not found / not a function");
		lua_pop(a_LuaState, 1);
		return 1;
	}
	lua_pushvalue(a_LuaState, -2);  // Copy the string that has been passed to us
	LOGD("Calling BreakIntoDebugger()...");
	lua_call(a_LuaState, 1, 0);
	LOGD("Returned from BreakIntoDebugger().");
	return 0;
}





void cLuaState::TrackRef(cTrackedRef & a_Ref)
{
	// Get the CanonLuaState global from Lua:
	auto canonState = QueryCanonLuaState();
	if (canonState == nullptr)
	{
		LOGWARNING("%s: Lua state %p has invalid CanonLuaState!", __FUNCTION__, static_cast<void *>(m_LuaState));
		return;
	}

	// Add the callback:
	cCSLock Lock(canonState->m_CSTrackedRefs);
	canonState->m_TrackedRefs.push_back(&a_Ref);
}





void cLuaState::UntrackRef(cTrackedRef & a_Ref)
{
	// Get the CanonLuaState global from Lua:
	auto canonState = QueryCanonLuaState();
	if (canonState == nullptr)
	{
		LOGWARNING("%s: Lua state %p has invalid CanonLuaState!", __FUNCTION__, static_cast<void *>(m_LuaState));
		return;
	}

	// Remove the callback (note that another thread may have cleared the callbacks by closing the LuaState):
	cCSLock Lock(canonState->m_CSTrackedRefs);
	auto & trackedRefs = canonState->m_TrackedRefs;
	for (auto itr = trackedRefs.begin(), end = trackedRefs.end(); itr != end; ++itr)
	{
		if (*itr == &a_Ref)
		{
			trackedRefs.erase(itr);
			break;
		}
	}
}





////////////////////////////////////////////////////////////////////////////////
// cLuaState::cRef:

cLuaState::cRef::cRef(void) :
	m_LuaState(nullptr),
	m_Ref(LUA_REFNIL)
{
}





cLuaState::cRef::cRef(cLuaState & a_LuaState, int a_StackPos) :
	m_LuaState(nullptr),
	m_Ref(LUA_REFNIL)
{
	RefStack(a_LuaState, a_StackPos);
}





cLuaState::cRef::cRef(cRef && a_FromRef):
	m_LuaState(a_FromRef.m_LuaState),
	m_Ref(a_FromRef.m_Ref)
{
	a_FromRef.m_LuaState = nullptr;
	a_FromRef.m_Ref = LUA_REFNIL;
}





cLuaState::cRef::~cRef()
{
	if (m_LuaState != nullptr)
	{
		UnRef();
	}
}





void cLuaState::cRef::RefStack(cLuaState & a_LuaState, int a_StackPos)
{
	ASSERT(a_LuaState.IsValid());
	if (m_LuaState != nullptr)
	{
		UnRef();
	}
	ASSERT(cCanonLuaStates::GetCanonState(a_LuaState)->m_CS.IsLockedByCurrentThread());
	m_LuaState = a_LuaState;
	lua_pushvalue(a_LuaState, a_StackPos);  // Push a copy of the value at a_StackPos onto the stack
	m_Ref = luaL_ref(a_LuaState, LUA_REGISTRYINDEX);
}





void cLuaState::cRef::UnRef(void)
{
	if (IsValid())
	{
		ASSERT(cCanonLuaStates::GetCanonState(m_LuaState)->m_CS.IsLockedByCurrentThread());
		luaL_unref(m_LuaState, LUA_REGISTRYINDEX, m_Ref);
	}
	m_LuaState = nullptr;
	m_Ref = LUA_REFNIL;
}




