module luad.base;

import luad.c.all;
import luad.stack;

import core.stdc.string : strlen;

/**
 * Enumerates all Lua types.
 */
enum LuaType
{
	///string
	String = LUA_TSTRING,
	///number
	Number = LUA_TNUMBER,
	//table
	Table = LUA_TTABLE,
	///nil
	Nil = LUA_TNIL,
	///boolean
	Boolean = LUA_TBOOLEAN,
	///function
	Function = LUA_TFUNCTION,
	///userdata
	Userdata = LUA_TUSERDATA,
	///ditto
	LightUserdata = LUA_TLIGHTUSERDATA,
	///thread
	Thread = LUA_TTHREAD
}

package struct Nil{}

/**
 * Special value representing the Lua type and value nil.
 * Examples:
 * Useful for clearing keys in a table:
 * --------------------------
	lua["n"] = 1.23;
	assert(lua.get!double("n") == 1.23);

	lua["n"] = nil;
	assert(lua["n"].type == LuaType.Nil);
 * --------------------------
 */
public Nil nil;

/**
 * Represents a reference to a Lua value of any type.
 * It contains only the bare minimum of functionality which all Lua values support.
 * For a generic reference type with more functionality, see $(DPREF dynamic,LuaDynamic).
 */
struct LuaObject
{
	private:
	int r = LUA_REFNIL;
	lua_State* L = null;

	package:
	this(lua_State* L, int idx)
	{
		this.L = L;

		lua_pushvalue(L, idx);
		r = luaL_ref(L, LUA_REGISTRYINDEX);
	}

	void push() nothrow
	{
		lua_rawgeti(L, LUA_REGISTRYINDEX, r);
	}

	static void checkType(lua_State* L, int idx, int expectedType, const(char)* expectedName)
	{
		int t = lua_type(L, idx);
		if(t != expectedType)
		{
			luaL_error(L, "attempt to create %s with %s", expectedName, lua_typename(L, t));
		}
	}

	public:
	@trusted this(this)
	{
		push();
		r = luaL_ref(L, LUA_REGISTRYINDEX);
	}

	@trusted nothrow ~this()
	{
		luaL_unref(L, LUA_REGISTRYINDEX, r);
	}

	/// The underlying $(D lua_State) pointer for interfacing with C.
	lua_State* state() pure nothrow @safe @property
	{
		return L;
	}

	/**
	 * Release this reference.
	 *
	 * This reference becomes a nil reference.
	 * This is only required when you want to _release the reference before the lifetime
	 * of this $(D LuaObject) has ended.
	 */
	void release() pure nothrow @safe
	{
		r = LUA_REFNIL;
		L = null;
	}

	/**
	 * Type of referenced object.
	 * See_Also:
	 *	 $(MREF LuaType)
	 */
	@property LuaType type() @trusted nothrow
	{
		push();
		auto result = cast(LuaType)lua_type(state, -1);
		lua_pop(state, 1);
		return result;
	}

	/**
	 * Type name of referenced object.
	 */
	@property string typeName() @trusted /+ nothrow +/
	{
		push();
		const(char)* cname = luaL_typename(state, -1); // TODO: Doesn't have to use luaL_typename, i.e. no copy
		auto name = cname[0.. strlen(cname)].idup;
		lua_pop(state, 1);
		return name;
	}

	/// Boolean whether or not the referenced object is nil.
	@property bool isNil() pure nothrow @safe
	{
		return r == LUA_REFNIL;
	}

	/**
	 * Convert the referenced object into a textual representation.
	 *
	 * The returned string is formatted in the same way the Lua $(D tostring) function formats.
	 *
	 * Returns:
	 * String representation of referenced object
	 */
	string toString() @trusted
	{
		push();

		size_t len;
		const(char)* cstr = luaL_tolstring(state, -1, &len);
		auto str = cstr[0 .. len].idup;

		lua_pop(state, 2);
		return str;
	}

	/**
	 * Attempt _to convert the referenced object _to the specified D type.
	 * Examples:
	 -----------------------
	auto results = lua.doString(`return "hello!"`);
	assert(results[0].to!string() == "hello!");
	 -----------------------
	 */
	T to(T)()
	{
		static void typeMismatch(lua_State* L, int t, int e)
		{
			luaL_error(L, "attempt to convert LuaObject with type %s to a %s", lua_typename(L, t), lua_typename(L, e));
		}

		push();
		return popValue!(T, typeMismatch)(state);
	}

	/**
	 * Compare this object to another with Lua's equality semantics.
	 * Also returns false if the two objects are in different Lua states.
	 */
	bool opEquals(T : LuaObject)(ref T o) @trusted
	{
		if(o.state != this.state)
			return false;

		push();
		o.push();
		scope(success) lua_pop(state, 2);

		return lua_equal(state, -1, -2);
	}
}

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

	lua_pushstring(L, "foobar");
	auto o = popValue!LuaObject(L);

	assert(!o.isNil);
	assert(o.type == LuaType.String);
	assert(o.typeName == "string");
	assert(o.to!string() == "foobar");

	lua_pushnil(L);
	auto nilref = popValue!LuaObject(L);

	assert(nilref.isNil);
	assert(nilref.typeName == "nil");

	assert(o != nilref);

	auto o2 = o;
	assert(o2 == o);
}
