/**
Internal module for pushing and getting _structs.

A struct is treated as a table layout schema.
Pushing a struct to Lua will create a table and fill it with key-value pairs - corresponding to struct fields - from the struct; the field name becomes the table key as a string.
Struct methods are treated as if they were delegate fields pointing to the method.
For an example, see the "Configuration File" example on the $(LINK2 $(REFERENCETOP),front page).
*/
module luad.conversions.structs;

import luad.c.all;

import luad.stack;

private template isInternal(string field)
{
	enum isInternal = field.length >= 2 && field[0..2] == "__";
}

//TODO: ignore static fields, post-blits, destructors, etc?
void pushStruct(T)(lua_State* L, ref T value) if (is(T == struct))
{
	lua_createtable(L, 0, value.tupleof.length);

	foreach(field; __traits(allMembers, T))
	{
		static if(!isInternal!field &&
		          field != "this" &&
		          field != "opAssign")
		{
			pushValue(L, field);

			enum isMemberFunction = mixin("is(typeof(&value." ~ field ~ ") == delegate)");

			static if(isMemberFunction)
				pushValue(L, mixin("&value." ~ field));
			else
				pushValue(L, mixin("value." ~ field));

			lua_settable(L, -3);
		}
	}
}

T getStruct(T)(lua_State* L, int idx) if(is(T == struct))
{
	T s;
	fillStruct(L, idx, s);
	return s;
}

void fillStruct(T)(lua_State* L, int idx, ref T s) if(is(T == struct))
{
	foreach(field; __traits(allMembers, T))
	{
		static if(field != "this" && !isInternal!(field))
		{
			static if(__traits(getOverloads, T, field).length == 0)
			{
				lua_getfield(L, idx, field.ptr);
				if(lua_isnil(L, -1) == 0) {
					mixin("s." ~ field ~
					  " = popValue!(typeof(s." ~ field ~ "))(L);");
				} else
					lua_pop(L, 1);
			}
		}
	}
}

version(unittest)
{
	import luad.base;
	struct S
	{
		LuaObject o;
		int i;
		double n;
		string s;

		string f(){ return "foobar"; }
	}
}

unittest
{
	import luad.testing;

	lua_State* L = luaL_newstate();
	scope(success) lua_close(L);
	luaL_openlibs(L);

	pushValue(L, "test");
	auto obj = popValue!LuaObject(L);

	pushValue(L, S(obj, 1, 2.3, "hello"));
	assert(lua_istable(L, -1));
	lua_setglobal(L, "struct");

	unittest_lua(L, `
		for key, expected in pairs{i = 1, n = 2.3, s = "hello"} do
			local value = struct[key]
			assert(value == expected,
				("bad table pair: '%s' = '%s' (expected '%s')"):format(key, value, expected)
			)
		end

		assert(struct.f() == "foobar")
	`);

	lua_getglobal(L, "struct");
	S s = getStruct!S(L, -1);

	assert(s.o == obj);
	assert(s.i == 1);
	assert(s.n == 2.3);
	assert(s.s == "hello");

	lua_pop(L, 1);

	struct S2
	{
		string a, b;
	}

	unittest_lua(L, `
		incompleteStruct = {a = "foo"}
	`);

	lua_getglobal(L, "incompleteStruct");
	S2 s2 = getStruct!S2(L, -1);

	assert(s2.a == "foo");
	assert(s2.b == null);

	lua_pop(L, 1);
}
