/*
 * Lunar - improved version of Luna for find binding C++-objects to Lua
 * From http://lua-users.org/wiki/CppBindingWithLunar
 *
 * Changes by Jonas Gehring <jonas.gehring@boolsoft.org>:
 *   * An optional table argument has been added to Lunar::Register()
 *   * Store instances in std::shared_ptr. This is handy for sharing object
 *     ownership between C++ and Lua.
 */


#ifndef LUNAR_H_
#define LUNAR_H_

#include <memory>

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

template <typename T> class Lunar
{
	typedef struct {
		std::shared_ptr<T> pT;
	} userdataType;

	public:
	typedef int (T::*mfp)(lua_State *L);
	typedef struct { const char *name; mfp mfunc; } RegType;

	static void Register(lua_State *L, const char *table = NULL) {
		lua_newtable(L);
		int methods = lua_gettop(L);

		luaL_newmetatable(L, T::className);
		int metatable = lua_gettop(L);

		// store method table in globals so that
		// scripts can add functions written in Lua.
		if (table == NULL) {
			lua_pushvalue(L, methods);
			set(L, LUA_GLOBALSINDEX, T::className);
		} else {
			lua_pushstring(L, table);
			lua_gettable(L, LUA_GLOBALSINDEX);
			if (lua_isnil(L, -1)) { // Don't create the table, but fail silently
				lua_pop(L, 1);
				lua_pushvalue(L, methods);
				set(L, LUA_GLOBALSINDEX, T::className);
			} else {
				lua_pushstring(L, T::className);
				lua_pushvalue(L, methods);
				lua_settable(L, -3);
			}
		}

		// hide metatable from Lua getmetatable()
		lua_pushvalue(L, methods);
		set(L, metatable, "__metatable");

		lua_pushvalue(L, methods);
		set(L, metatable, "__index");

		lua_pushcfunction(L, tostring_T);
		set(L, metatable, "__tostring");

		lua_pushcfunction(L, gc_T);
		set(L, metatable, "__gc");

		lua_newtable(L);                // mt for method table
		lua_pushcfunction(L, new_T);
		lua_pushvalue(L, -1);           // dup new_T function
		set(L, methods, "new");         // add new_T to method table
		set(L, -3, "__call");           // mt.__call = new_T
		lua_setmetatable(L, methods);

		// fill method table with methods from class T
		for (RegType *l = T::methods; l->name; l++) {
			lua_pushstring(L, l->name);
			lua_pushlightuserdata(L, (void*)l);
			lua_pushcclosure(L, thunk, 1);
			lua_settable(L, methods);
		}

		lua_pop(L, 2);  // drop metatable and method table
	}

	// call named lua method from userdata method table
	static int call(lua_State *L, const char *method,
			int nargs=0, int nresults=LUA_MULTRET, int errfunc=0)
	{
		int base = lua_gettop(L) - nargs;  // userdata index
		if (!luaL_checkudata(L, base, T::className)) {
			lua_settop(L, base-1);           // drop userdata and args
			lua_pushfstring(L, "not a valid %s userdata", T::className);
			return -1;
		}

		lua_pushstring(L, method);         // method name
		lua_gettable(L, base);             // get method from userdata
		if (lua_isnil(L, -1)) {            // no method?
			lua_settop(L, base-1);           // drop userdata and args
			lua_pushfstring(L, "%s missing method '%s'", T::className, method);
			return -1;
		}
		lua_insert(L, base);               // put method under userdata, args

		int status = lua_pcall(L, 1+nargs, nresults, errfunc);  // call method
		if (status) {
			const char *msg = lua_tostring(L, -1);
			if (msg == NULL) msg = "(error with no message)";
			lua_pushfstring(L, "%s:%s status = %d\n%s",
					T::className, method, status, msg);
			lua_remove(L, base);             // remove old message
			return -1;
		}
		return lua_gettop(L) - base + 1;   // number of results
	}

	// push onto the Lua stack a userdata containing a pointer to T object.
	// This will wrap the pointer in a shared_ptr instance. If gc is false, the
	// shared_ptr will not own the object pointer (this is realized by not
	// calling the shared pointers' destructor in gc_T()).
	static int push(lua_State *L, T *obj, bool gc=false) {
		if (!obj) { lua_pushnil(L); return 0; }
		userdataType *ud = NULL;
		int mt = preparePush(L, obj, &ud);
		if (ud) {
			new (&ud->pT) std::shared_ptr<T>(obj);
			mt = doPush(L, mt, gc);
		}
		return mt;
	}

	// push onto the Lua stack a userdata containing a pointer to T object.
	// This will create another referencing shared_ptr that will be destroyed
	// once the userdata is garbage collected.
	static int push(lua_State *L, std::shared_ptr<T> ptr) {
		if (!ptr) { lua_pushnil(L); return 0; }
		userdataType *ud = NULL;
		int mt = preparePush(L, ptr.get(), &ud);
		if (ud) {
			new (&ud->pT) std::shared_ptr<T>(ptr);
			mt = doPush(L, mt, true);
		}
		return mt;
	}

	// get userdata from Lua stack and return pointer to T object
	static T *check(lua_State *L, int narg) {
		userdataType *ud =
			static_cast<userdataType*>(luaL_checkudata(L, narg, T::className));
		if(!ud) {
			luaL_typerror(L, narg, T::className);
			return NULL;
		}
		return ud->pT.get();  // pointer to T object
	}

	private:
	Lunar();  // hide default constructor

	// member function dispatcher
	static int thunk(lua_State *L) {
		// stack has userdata, followed by method args
		T *obj = check(L, 1);  // get 'self', or if you prefer, 'this'

		// In order to prevent garbage collection of this object while the function
		// is running, store a reference in the global index.
		char buf[34];
		sprintf(buf, "__%p", (void*)obj);
		lua_pushvalue(L, 1);
		lua_setglobal(L, buf);

		lua_remove(L, 1);  // remove self so member function args start at index 1
		// get member function from upvalue
		RegType *l = static_cast<RegType*>(lua_touserdata(L, lua_upvalueindex(1)));
		int ret = (obj->*(l->mfunc))(L);  // call member function

		// Remove temporary reference
		lua_pushnil(L);
		lua_setglobal(L, buf);

		return ret;
	}

	// create a new T object and
	// push onto the Lua stack a userdata containing a pointer to T object
	static int new_T(lua_State *L) {
		lua_remove(L, 1);   // use classname:new(), instead of classname.new()
		T *obj = new T(L);  // call constructor for T objects
		push(L, obj, true); // gc_T will delete this object
		return 1;           // userdata containing pointer to T object
	}

	// garbage collection metamethod
	static int gc_T(lua_State *L) {
		if (luaL_getmetafield(L, 1, "do not trash")) {
			lua_pushvalue(L, 1);  // dup userdata
			lua_gettable(L, -2);
			if (!lua_isnil(L, -1)) return 0;  // do not delete object
		}
		userdataType *ud = static_cast<userdataType*>(lua_touserdata(L, 1));
		ud->pT.reset(); // remove shared_ptr reference
		return 0;
	}

	static int tostring_T (lua_State *L) {
		char buff[32];
		userdataType *ud = static_cast<userdataType*>(lua_touserdata(L, 1));
		T *obj = ud->pT.get();
		sprintf(buff, "%p", (void*)obj);
		lua_pushfstring(L, "%s (%s)", T::className, buff);

		return 1;
	}

	static int preparePush(lua_State *L, T *obj, userdataType **ud) {
		luaL_getmetatable(L, T::className);  // lookup metatable in Lua registry
		if (lua_isnil(L, -1)) luaL_error(L, "%s missing metatable", T::className);
		int mt = lua_gettop(L);
		subtable(L, mt, "userdata", "v");
		*ud = static_cast<userdataType*>(pushuserdata(L, obj, sizeof(userdataType)));
		return mt;
	}

	static int doPush(lua_State *L, int mt, bool gc=false) {
		lua_pushvalue(L, mt);
		lua_setmetatable(L, -2);
		if (gc == false) {
			lua_checkstack(L, 3);
			subtable(L, mt, "do not trash", "k");
			lua_pushvalue(L, -2);
			lua_pushboolean(L, 1);
			lua_settable(L, -3);
			lua_pop(L, 1);
		}
		lua_replace(L, mt);
		lua_settop(L, mt);
		return mt;  // index of userdata containing pointer to T object
	}


	static void set(lua_State *L, int table_index, const char *key) {
		lua_pushstring(L, key);
		lua_insert(L, -2);  // swap value and key
		lua_settable(L, table_index);
	}

	static void weaktable(lua_State *L, const char *mode) {
		lua_newtable(L);
		lua_pushvalue(L, -1);  // table is its own metatable
		lua_setmetatable(L, -2);
		lua_pushliteral(L, "__mode");
		lua_pushstring(L, mode);
		lua_settable(L, -3);   // metatable.__mode = mode
	}

	static void subtable(lua_State *L, int tindex, const char *name, const char *mode) {
		lua_pushstring(L, name);
		lua_gettable(L, tindex);
		if (lua_isnil(L, -1)) {
			lua_pop(L, 1);
			lua_checkstack(L, 3);
			weaktable(L, mode);
			lua_pushstring(L, name);
			lua_pushvalue(L, -2);
			lua_settable(L, tindex);
		}
	}

	static void *pushuserdata(lua_State *L, void *key, size_t sz) {
		void *ud = 0;
		lua_pushlightuserdata(L, key);
		lua_gettable(L, -2);     // lookup[key]
		if (lua_isnil(L, -1)) {
			lua_pop(L, 1);         // drop nil
			lua_checkstack(L, 3);
			ud = lua_newuserdata(L, sz);  // create new userdata
			lua_pushlightuserdata(L, key);
			lua_pushvalue(L, -2);  // dup userdata
			lua_settable(L, -4);   // lookup[key] = userdata
		}
		return ud;
	}
};

#define LUNAR_DECLARE_METHOD(Class, Name) {#Name, &Class::Name}


#endif // LUNAR_H_
