module luad.lfunction;

import luad.base;
import luad.table;
import luad.stack;
import luad.conversions.functions;

import luad.c.all;

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

	version(none) package this(lua_State* L, int idx)
	{
		LuaObject.checkType(L, idx, LUA_TFUNCTION, "LuaFunction");
		object = LuaObject(L, idx);
	}

	/**
	 * Call this function and collect all return values as
	 * an array of $(DPREF base, LuaObject) references.
	 * Examples:
	 -----------------------
	lua.doString(`function f(...) return ... end`);
	auto f = lua.get!LuaFunction("f");

	LuaObject[] ret = f(1.2, "hello!", true);

	assert(ret[0].to!double() == 1.2);
	assert(ret[1].to!string() == "hello!");
	assert(ret[2].to!bool());
	 -----------------------
	 */
	LuaObject[] opCall(U...)(U args)
	{
		return call!(LuaVariableReturn!(LuaObject[]))(args).returnValues;
	}

	/**
	 * Call this function.
	 * Params:
	 *	 T = expected return type.
	 *	 args = list of arguments.
	 * Returns:
	 *	 Return value of type $(D T), or nothing if $(D T) was unspecified.
	 *   See $(DPMODULE2 conversions,functions) for how to
	 *   catch multiple return values.
	 * Examples:
	 * ------------------
	lua.doString(`function ask(question) return 42 end`);
	auto ask = lua.get!LuaFunction("ask");

	auto answer = ask.call!int("What's the answer to life, the universe and everything?");
	assert(answer == 42);
	 * ------------------
	 */
	T call(T = void, U...)(U args)
	{
		this.push();
		foreach(arg; args)
			pushValue(this.state, arg);

		return callWithRet!T(this.state, args.length);
	}

	/**
	 * Set a new environment for this function.
	 *
	 * The environment of a function is the table used for looking up non-local (global) variables.
	 * Params:
	 *    env = new environment
	 * Examples:
	 * -------------------
	 * lua["foo"] = "bar";
	 * auto func = lua.loadString(`return foo`);
	 * assert(func.call!string() == "bar");
	 *
	 * auto env = lua.wrap(["foo": "test"]);
	 * func.setEnvironment(env);
	 * assert(func.call!string() == "test");
	 * -------------------
	 */
	void setEnvironment(ref LuaTable env)
	in { assert(this.state == env.state); }
	body
	{
		this.push();
		env.push();
		lua_setfenv(this.state, -2);
		lua_pop(this.state, 1);
	}

	/**
	 * Dump this function as a binary chunk of Lua bytecode to the specified
	 * writer delegate.  Multiple chunks may be produced to dump a single
	 * function.
	 *
	 * Params:
	 *    writer = delegate to forward writing calls to
	 *
	 *  If the delegate returns $(D false) for any of the chunks,
	 *  the _dump process ends, and the writer won't be called again.
	 */
	bool dump(scope bool delegate(in void[]) writer)
	{
		alias typeof(writer) LuaWriter;

		extern(C) static int luaCWriter(lua_State* L, const void* p, size_t sz, void* ud)
		{
			auto writer = *cast(LuaWriter*)ud;
			return writer(p[0..sz]) ? 0 : 1;
		}

		this.push();
		auto ret = lua_dump(this.state, &luaCWriter, &writer);
		lua_pop(this.state, 1);
		return ret == 0;
	}
}

version(unittest)
{
	import luad.testing;
	import std.variant;
	import std.typecons;
}

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

	lua_getglobal(L, "tostring");
	auto tostring = popValue!LuaFunction(L);

	LuaObject[] ret = tostring(123);
	assert(ret[0].to!string() == "123");

	assert(tostring.call!string(123) == "123");

	tostring.call(321);

	// Multiple return values
	luaL_dostring(L, "function singleRet() return 42 end");
	lua_getglobal(L, "singleRet");
	auto singleRet = popValue!LuaFunction(L);

	auto singleRetResult = singleRet.call!(Tuple!int)();
	assert(singleRetResult[0] == 42);

	alias Algebraic!(string, double) BasicLuaType;
	BasicLuaType a = "foo";
	BasicLuaType b = 1.5;

	pushValue(L, [a, b]);
	lua_setglobal(L, "test");

	luaL_dostring(L, "function multRet() return unpack(test) end");
	lua_getglobal(L, "multRet");
	auto multRet = popValue!LuaFunction(L);

	auto result = multRet.call!(Tuple!(string, double))();
	assert(result[0] == a);
	assert(result[1] == b);

	unittest_lua(L, `function getName() return "Foo", "Bar" end`);

	lua_getglobal(L, "getName");
	auto getName = popValue!LuaFunction(L);

	string[2] arrayRet = getName.call!(string[2])();
	assert(arrayRet[0] == "Foo");
	assert(arrayRet[1] == "Bar");

	// setEnvironment
	pushValue(L, ["test": [42]]);
	auto env = popValue!LuaTable(L);

	lua_getglobal(L, "unpack");
	env["unpack"] = popValue!LuaObject(L);

	multRet.setEnvironment(env);
	assert(multRet.call!int() == 42);

}
