module luad.table;

import luad.c.all;

import luad.base;
import luad.stack;
import luad.conversions.structs;

/// Represents a Lua table.
struct LuaTable
{
	/// LuaTable sub-types $(DPREF base, LuaObject) through this reference.
	LuaObject object;

	alias object this;

	package this(lua_State* L, int idx)
	{
		LuaObject.checkType(L, idx, LUA_TTABLE, "LuaTable");
		object = LuaObject(L, idx);
	}

	/**
	 * Lookup a value in this table or in a sub-table of this table.
	 * Params:
	 *	 T = type of value
	 *	 args = list of keys, where all keys but the last one should result in a table
	 * Returns:
	 *	 $(D t[k]) where $(D t) is the table for the second-to-last parameter, and $(D k) is the last parameter
	 *
	 * Examples:
	 * ----------------------
	auto execute = lua.get!LuaFunction("os", "execute");
	execute(`echo hello, world!`);
	 * ----------------------
	 */
	T get(T, U...)(U args) @trusted
	{
		this.push();

		foreach(key; args)
		{
			pushValue(this.state, key);
			lua_gettable(this.state, -2);
		}

		auto ret = getValue!T(this.state, -1);
		lua_pop(this.state, args.length + 1);
		return ret;
	}

	/**
	 * Read a string value in this table without making a copy of the string.
	 * The read string is passed to $(D dg), and should not be escaped.
	 * If the value for $(D key) is not a string, $(D dg) is not called.
	 * Params:
	 *    key = lookup _key
	 *    dg = delegate to receive string
	 * Returns:
	 *    $(D true) if the value for $(D key) was a string and passed to $(D dg), $(D false) otherwise
	 * Examples:
	 --------------------
	t[2] = "two";
	t.readString(2, str => assert(str == "two"));
	 --------------------
	 */
	bool readString(T)(T key, scope void delegate(in char[] str) dg) @trusted
	{
		this.push();
		scope(exit) lua_pop(this.state, 1);

		pushValue(this.state, key);

		lua_gettable(this.state, -2);
		scope(exit) lua_pop(this.state, 1);

		if(lua_isstring(this.state, -1) == 0)
			return false;

		size_t len;
		const(char)* cstr = lua_tolstring(this.state, -1, &len);

		dg(cstr[0 .. len]);
		return true;
	}

	/**
	 * Same as calling $(D get!LuaObject) with the same arguments.
	 * Examples:
	 * ---------------------
	auto luapath = lua["package", "path"];
	writefln("LUA_PATH:\n%s", luapath);
	 * ---------------------
	 * See_Also:
	 *   $(MREF LuaTable.get)
	 */
	LuaObject opIndex(T...)(T args)
	{
		return get!LuaObject(args);
	}

	/**
	 * Set a key-value pair in this table.
	 * Params:
	 *	 key = key to _set
	 *	 value = value for $(D key)
	 */
	void set(T, U)(T key, U value) @trusted
	{
		this.push();
		scope(success) lua_pop(this.state, 1);

		pushValue(this.state, key);
		pushValue(this.state, value);
		lua_settable(this.state, -3);
	}

	/**
	 * Set a key-value pair this table or in a sub-table of this table.
	 * Params:
	 *	 value = value to set
	 *	 args = list of keys, where all keys but the last one should result in a table
	 * Returns:
	 *	 $(D t[k] = value), where $(D t) is the table for the second-to-last parameter in args,
	 *	 and $(D k) is the last parameter in args
	 *
	 * Examples:
	 * ----------------------
	lua["string", "empty"] = (in char[] s){ return s.length == 0; };
	lua.doString(`assert(string.empty(""))`);
	 * ----------------------
	 */
	void opIndexAssign(T, U...)(T value, U args) @trusted
	{
		this.push();
		scope(success) lua_pop(this.state, 1);

		foreach(i, arg; args)
		{
			static if(i != args.length - 1)
			{
				pushValue(this.state, arg);
				lua_gettable(this.state, -2);
			}
		}

		pushValue(this.state, args[$-1]);
		pushValue(this.state, value);
		lua_settable(this.state, -3);

		lua_pop(this.state, args.length - 1);
	}

	/**
	 * Create struct of type $(D T) and fill its members with fields from this table.
	 *
	 * Struct fields that are not present in this table are left at their default value.
	 *
	 * Params:
	 *	 T = any struct type
	 *
	 * Returns:
	 *	 Newly created struct
	 */
	T toStruct(T)() @trusted if (is(T == struct))
	{
		push();
		return popValue!T(this.state);
	}

	/**
	 * Fill a struct's members with fields from this table.
	 * Params:
	 *	 s = struct to fill
	 */
	void copyTo(T)(ref T s) @trusted if (is(T == struct))
	{
		push();
		fillStruct(this.state, -1, s);
		lua_pop(L, 1);
	}

	/**
	 * Set the metatable for this table.
	 * Params:
	 *	 meta = new metatable
 	 */
	void setMetaTable(ref LuaTable meta) @trusted
	in{ assert(this.state == meta.state); }
	body
	{
		this.push();
		meta.push();
		lua_setmetatable(this.state, -2);
		lua_pop(this.state, 1);
	}

	/**
	 * Get the metatable for this table.
	 * Returns:
	 *	 A reference to the metatable for this table. The reference is nil if this table has no metatable.
	 */
	LuaTable getMetaTable() @trusted
	{
		this.push();
		scope(success) lua_pop(this.state, 1);

		return lua_getmetatable(this.state, -1) == 0? LuaTable() : popValue!LuaTable(this.state);
	}

	/**
	* Get the array length of the table.
	*/
	size_t length() @trusted
	{
		this.push();
		size_t len = lua_objlen(this.state, -1);
		lua_pop(this.state, 1);
		return len;
	}

	/**
	 * Iterate over the values in this table.
	 */
	int opApply(T)(int delegate(ref T value) dg) @trusted
	{
		this.push();
		lua_pushnil(this.state);
		while(lua_next(this.state, -2) != 0)
		{
			auto value = popValue!T(this.state);
			int result = dg(value);
			if(result != 0)
			{
				lua_pop(this.state, 2);
				return result;
			}
		}
		lua_pop(this.state, 1);
		return 0;
	}

	/**
	 * Iterate over the key-value pairs in this table.
	 */
	int opApply(T, U)(int delegate(ref U key, ref T value) dg) @trusted
	{
		this.push();
		lua_pushnil(this.state);
		while(lua_next(this.state, -2) != 0)
		{
			auto value = popValue!T(this.state);
			auto key = getValue!U(this.state, -1);

			int result = dg(key, value);
			if(result != 0)
			{
				lua_pop(this.state, 2);
				return result;
			}
		}
		lua_pop(this.state, 1);
		return 0;
	}
}

unittest
{
	lua_State* L = luaL_newstate();
	scope(success)
	{
		assert(lua_gettop(L) == 0);
		lua_close(L);
	}

	lua_newtable(L);
	auto t = popValue!LuaTable(L);

	assert(t.type == LuaType.Table);

	t.set("foo", "bar");
	assert(t.get!string("foo") == "bar");

	t.set("foo", nil);
	assert(t.get!LuaObject("foo").isNil);

	t.set("foo", ["outer": ["inner": "hi!"]]);
	auto s = t.get!(string)("foo", "outer", "inner");
	assert(s == "hi!");

	auto o = t["foo", "outer"];
	assert(o.type == LuaType.Table);

	t["foo", "outer", "inner"] = "hello!";
	auto s2 = t.get!(string)("foo", "outer", "inner");
	assert(s2 == "hello!");

	// length
	t.set("array", ["one", "two"]);
	auto a = t.get!LuaTable("array");
	assert(a.length == 2);

	// readString
	t[2] = "two";
	bool success = t.readString(2, (in char[] str) {
		assert(str == "two");
	});
	assert(success);

	t[2] = true;
	success = t.readString(2, (in char[] str) { assert(false); });
	assert(!success);

	// metatable
	pushValue(L, ["__index": (LuaObject self, string key){
		return key;
	}]);
	auto meta = popValue!LuaTable(L);

	lua_newtable(L);
	auto t2 = popValue!LuaTable(L);

	t2.setMetaTable(meta);

	auto test = t2.get!string("foobar");
	assert(test == "foobar");

	assert(t2.getMetaTable() == meta);

	// opApply
	auto input = [1, 2, 3];
	pushValue(L, input);
	auto applyTest = popValue!LuaTable(L);

	int i = 0;
	foreach(int v; applyTest)
	{
		assert(input[i++] == v);
	}

	auto inputWithKeys = ["one": 1, "two": 2, "three": 3];
	pushValue(L, inputWithKeys);
	auto applyTestKeys = popValue!LuaTable(L);

	foreach(string key, int value; applyTestKeys)
	{
		assert(inputWithKeys[key] == value);
	}
}
