/* tolua: event functions
** Support code for Lua bindings.
** Written by Waldemar Celes
** TeCGraf/PUC-Rio
** Apr 2003
*/

/* This code is free software; you can redistribute it and/or modify it.
** The software provided hereunder is on an "as is" basis, and
** the author has no obligation to provide maintenance, support, updates,
** enhancements, or modifications.
*/

#include <stdio.h>

#include "tolua++.h"

/* Store at ubox
	* It stores, creating the corresponding table if needed,
	* the pair key/value in the corresponding ubox table
*/
static void storeatubox (lua_State* L, int lo)
{
	#ifdef LUA_VERSION_NUM
		lua_getfenv(L, lo);
		if (lua_rawequal(L, -1, TOLUA_NOPEER)) {
			lua_pop(L, 1);
			lua_newtable(L);
			lua_pushvalue(L, -1);
			lua_setfenv(L, lo);	/* stack: k,v,table  */
		};
		lua_insert(L, -3);
		lua_settable(L, -3); /* on lua 5.1, we trade the "tolua_peers" lookup for a settable call */
		lua_pop(L, 1);
	#else
	 /* stack: key value (to be stored) */
		lua_pushstring(L,"tolua_peers");
		lua_rawget(L,LUA_REGISTRYINDEX);        /* stack: k v ubox */
		lua_pushvalue(L,lo);
		lua_rawget(L,-2);                       /* stack: k v ubox ubox[u] */
		if (!lua_istable(L,-1))
		{
			lua_pop(L,1);                          /* stack: k v ubox */
			lua_newtable(L);                       /* stack: k v ubox table */
			lua_pushvalue(L,1);
			lua_pushvalue(L,-2);                   /* stack: k v ubox table u table */
			lua_rawset(L,-4);                      /* stack: k v ubox ubox[u]=table */
		}
		lua_insert(L,-4);                       /* put table before k */
		lua_pop(L,1);                           /* pop ubox */
		lua_rawset(L,-3);                       /* store at table */
		lua_pop(L,1);                           /* pop ubox[u] */
	#endif
}

/* Module index function
*/
static int module_index_event (lua_State* L)
{
	lua_pushstring(L,".get");
	lua_rawget(L,-3);
	if (lua_istable(L,-1))
	{
		lua_pushvalue(L,2);  /* key */
		lua_rawget(L,-2);
		if (lua_iscfunction(L,-1))
		{
			lua_call(L,0,1);
			return 1;
		}
		else if (lua_istable(L,-1))
			return 1;
	}
	/* call old index meta event */
	if (lua_getmetatable(L,1))
	{
		lua_pushstring(L,"__index");
		lua_rawget(L,-2);
		lua_pushvalue(L,1);
		lua_pushvalue(L,2);
		if (lua_isfunction(L,-1))
		{
			lua_call(L,2,1);
			return 1;
		}
		else if (lua_istable(L,-1))
		{
			lua_gettable(L,-3);
			return 1;
		}
	}
	lua_pushnil(L);
	return 1;
}

/* Module newindex function
*/
static int module_newindex_event (lua_State* L)
{
	lua_pushstring(L,".set");
	lua_rawget(L,-4);
	if (lua_istable(L,-1))
	{
		lua_pushvalue(L,2);  /* key */
		lua_rawget(L,-2);
		if (lua_iscfunction(L,-1))
		{
			lua_pushvalue(L,1); /* only to be compatible with non-static vars */
			lua_pushvalue(L,3); /* value */
			lua_call(L,2,0);
			return 0;
		}
	}
	/* call old newindex meta event */
	if (lua_getmetatable(L,1) && lua_getmetatable(L,-1))
	{
		lua_pushstring(L,"__newindex");
		lua_rawget(L,-2);
		if (lua_isfunction(L,-1))
		{
		 lua_pushvalue(L,1);
		 lua_pushvalue(L,2);
		 lua_pushvalue(L,3);
			lua_call(L,3,0);
		}
	}
	lua_settop(L,3);
	lua_rawset(L,-3);
	return 0;
}

/* Class index function
	* If the object is a userdata (ie, an object), it searches the field in
	* the alternative table stored in the corresponding "ubox" table.
*/
static int class_index_event (lua_State* L)
{
 int t = lua_type(L,1);
	if (t == LUA_TUSERDATA)
	{
		/* Access alternative table */
		#ifdef LUA_VERSION_NUM /* new macro on version 5.1 */
		lua_getfenv(L,1);
		if (!lua_rawequal(L, -1, TOLUA_NOPEER)) {
			lua_pushvalue(L, 2); /* key */
			lua_gettable(L, -2); /* on lua 5.1, we trade the "tolua_peers" lookup for a gettable call */
			if (!lua_isnil(L, -1))
				return 1;
		};
		#else
		lua_pushstring(L,"tolua_peers");
		lua_rawget(L,LUA_REGISTRYINDEX);        /* stack: obj key ubox */
		lua_pushvalue(L,1);
		lua_rawget(L,-2);                       /* stack: obj key ubox ubox[u] */
		if (lua_istable(L,-1))
		{
			lua_pushvalue(L,2);  /* key */
			lua_rawget(L,-2);                      /* stack: obj key ubox ubox[u] value */
			if (!lua_isnil(L,-1))
				return 1;
		}
		#endif
		lua_settop(L,2);                        /* stack: obj key */
		/* Try metatables */
		lua_pushvalue(L,1);                     /* stack: obj key obj */
		while (lua_getmetatable(L,-1))
		{                                       /* stack: obj key obj mt */
			lua_remove(L,-2);                      /* stack: obj key mt */
			if (lua_isnumber(L,2))                 /* check if key is a numeric value */
			{
				/* try operator[] */
				lua_pushstring(L,".geti");
				lua_rawget(L,-2);                      /* stack: obj key mt func */
				if (lua_isfunction(L,-1))
				{
					lua_pushvalue(L,1);
					lua_pushvalue(L,2);
					lua_call(L,2,1);
					return 1;
				}
			}
			else
			{
			 lua_pushvalue(L,2);                    /* stack: obj key mt key */
				lua_rawget(L,-2);                      /* stack: obj key mt value */
				if (!lua_isnil(L,-1))
					return 1;
				else
					lua_pop(L,1);
				/* try C/C++ variable */
				lua_pushstring(L,".get");
				lua_rawget(L,-2);                      /* stack: obj key mt tget */
				if (lua_istable(L,-1))
				{
					lua_pushvalue(L,2);
					lua_rawget(L,-2);                      /* stack: obj key mt value */
					if (lua_iscfunction(L,-1))
					{
						lua_pushvalue(L,1);
						lua_pushvalue(L,2);
						lua_call(L,2,1);
						return 1;
					}
					else if (lua_istable(L,-1))
					{
						/* deal with array: create table to be returned and cache it in ubox */
						void* u = *((void**)lua_touserdata(L,1));
						lua_newtable(L);                /* stack: obj key mt value table */
						lua_pushstring(L,".self");
						lua_pushlightuserdata(L,u);
						lua_rawset(L,-3);               /* store usertype in ".self" */
						lua_insert(L,-2);               /* stack: obj key mt table value */
						lua_setmetatable(L,-2);         /* set stored value as metatable */
						lua_pushvalue(L,-1);            /* stack: obj key met table table */
						lua_pushvalue(L,2);             /* stack: obj key mt table table key */
						lua_insert(L,-2);               /*  stack: obj key mt table key table */
						storeatubox(L,1);               /* stack: obj key mt table */
						return 1;
					}
				}
			}
			lua_settop(L,3);
		}
		lua_pushnil(L);
		return 1;
	}
	else if (t== LUA_TTABLE)
	{
		module_index_event(L);
		return 1;
	}
	lua_pushnil(L);
	return 1;
}

/* Newindex function
	* It first searches for a C/C++ varaible to be set.
	* Then, it either stores it in the alternative ubox table (in the case it is
	* an object) or in the own table (that represents the class or module).
*/
static int class_newindex_event (lua_State* L)
{
 int t = lua_type(L,1);
	if (t == LUA_TUSERDATA)
	{
	 /* Try accessing a C/C++ variable to be set */
		lua_getmetatable(L,1);
		while (lua_istable(L,-1))                /* stack: t k v mt */
		{
			if (lua_isnumber(L,2))                 /* check if key is a numeric value */
			{
				/* try operator[] */
				lua_pushstring(L,".seti");
				lua_rawget(L,-2);                      /* stack: obj key mt func */
				if (lua_isfunction(L,-1))
				{
					lua_pushvalue(L,1);
					lua_pushvalue(L,2);
					lua_pushvalue(L,3);
					lua_call(L,3,0);
					return 0;
				}
			}
			else
			{
				lua_pushstring(L,".set");
				lua_rawget(L,-2);                      /* stack: t k v mt tset */
				if (lua_istable(L,-1))
				{
					lua_pushvalue(L,2);
					lua_rawget(L,-2);                     /* stack: t k v mt tset func */
					if (lua_iscfunction(L,-1))
					{
						lua_pushvalue(L,1);
						lua_pushvalue(L,3);
						lua_call(L,2,0);
						return 0;
					}
					lua_pop(L,1);                          /* stack: t k v mt tset */
				}
				lua_pop(L,1);                           /* stack: t k v mt */
				if (!lua_getmetatable(L,-1))            /* stack: t k v mt mt */
					lua_pushnil(L);
				lua_remove(L,-2);                       /* stack: t k v mt */
			}
		}
	 lua_settop(L,3);                          /* stack: t k v */

		/* then, store as a new field */
		storeatubox(L,1);
	}
	else if (t== LUA_TTABLE)
	{
		module_newindex_event(L);
	}
	return 0;
}

static int class_call_event(lua_State* L) {

	if (lua_istable(L, 1)) {
		lua_pushstring(L, ".call");
		lua_rawget(L, 1);
		if (lua_isfunction(L, -1)) {

			lua_insert(L, 1);
			lua_call(L, lua_gettop(L)-1, 1);

			return 1;
		};
	};
	tolua_error(L,"Attempt to call a non-callable object.",NULL);
	return 0;
};

static int do_operator (lua_State* L, const char* op)
{
	if (lua_isuserdata(L,1))
	{
		/* Try metatables */
		lua_pushvalue(L,1);                     /* stack: op1 op2 */
		while (lua_getmetatable(L,-1))
		{                                       /* stack: op1 op2 op1 mt */
			lua_remove(L,-2);                      /* stack: op1 op2 mt */
			lua_pushstring(L,op);                  /* stack: op1 op2 mt key */
			lua_rawget(L,-2);                      /* stack: obj key mt func */
			if (lua_isfunction(L,-1))
			{
				lua_pushvalue(L,1);
				lua_pushvalue(L,2);
				lua_call(L,2,1);
				return 1;
			}
			lua_settop(L,3);
		}
	}
	tolua_error(L,"Attempt to perform operation on an invalid operand",NULL);
	return 0;
}

static int class_add_event (lua_State* L)
{
	return do_operator(L,".add");
}

static int class_sub_event (lua_State* L)
{
	return do_operator(L,".sub");
}

static int class_mul_event (lua_State* L)
{
	return do_operator(L,".mul");
}

static int class_div_event (lua_State* L)
{
	return do_operator(L,".div");
}

static int class_lt_event (lua_State* L)
{
	return do_operator(L,".lt");
}

static int class_le_event (lua_State* L)
{
	return do_operator(L,".le");
}

static int class_eq_event (lua_State* L)
{
	/* copying code from do_operator here to return false when no operator is found */
	if (lua_isuserdata(L,1))
	{
		/* Try metatables */
		lua_pushvalue(L,1);                     /* stack: op1 op2 */
		while (lua_getmetatable(L,-1))
		{                                       /* stack: op1 op2 op1 mt */
			lua_remove(L,-2);                      /* stack: op1 op2 mt */
			lua_pushstring(L,".eq");                  /* stack: op1 op2 mt key */
			lua_rawget(L,-2);                      /* stack: obj key mt func */
			if (lua_isfunction(L,-1))
			{
				lua_pushvalue(L,1);
				lua_pushvalue(L,2);
				lua_call(L,2,1);
				return 1;
			}
			lua_settop(L,3);
		}
	}

	lua_settop(L, 3);
	lua_pushboolean(L, 0);
	return 1;
}

/*
static int class_gc_event (lua_State* L)
{
	void* u = *((void**)lua_touserdata(L,1));
	fprintf(stderr, "collecting: looking at %p\n", u);
	lua_pushstring(L,"tolua_gc");
	lua_rawget(L,LUA_REGISTRYINDEX);
	lua_pushlightuserdata(L,u);
	lua_rawget(L,-2);
	if (lua_isfunction(L,-1))
	{
		lua_pushvalue(L,1);
		lua_call(L,1,0);
 		lua_pushlightuserdata(L,u);
		lua_pushnil(L);
		lua_rawset(L,-3);
	}
	lua_pop(L,2);
	return 0;
}
*/
TOLUA_API int class_gc_event (lua_State* L)
{
	void* u = *((void**)lua_touserdata(L,1));
	int top;
	/*fprintf(stderr, "collecting: looking at %p\n", u);*/
	/*
	lua_pushstring(L,"tolua_gc");
	lua_rawget(L,LUA_REGISTRYINDEX);
	*/
	lua_pushvalue(L, lua_upvalueindex(1));
	lua_pushlightuserdata(L,u);
	lua_rawget(L,-2);            /* stack: gc umt    */
	lua_getmetatable(L,1);       /* stack: gc umt mt */
	/*fprintf(stderr, "checking type\n");*/
	top = lua_gettop(L);
	if (tolua_fast_isa(L,top,top-1, lua_upvalueindex(2))) /* make sure we collect correct type */
	{
		/*fprintf(stderr, "Found type!\n");*/
		/* get gc function */
		lua_pushliteral(L,".collector");
		lua_rawget(L,-2);           /* stack: gc umt mt collector */
		if (lua_isfunction(L,-1)) {
			/*fprintf(stderr, "Found .collector!\n");*/
		}
		else {
			lua_pop(L,1);
			/*fprintf(stderr, "Using default cleanup\n");*/
			lua_pushcfunction(L,tolua_default_collect);
		}

		lua_pushvalue(L,1);         /* stack: gc umt mt collector u */
		lua_call(L,1,0);

		lua_pushlightuserdata(L,u); /* stack: gc umt mt u */
		lua_pushnil(L);             /* stack: gc umt mt u nil */
		lua_rawset(L,-5);           /* stack: gc umt mt */
	}
	lua_pop(L,3);
	return 0;
}


/* Register module events
	* It expects the metatable on the top of the stack
*/
TOLUA_API void tolua_moduleevents (lua_State* L)
{
	lua_pushstring(L,"__index");
	lua_pushcfunction(L,module_index_event);
	lua_rawset(L,-3);
	lua_pushstring(L,"__newindex");
	lua_pushcfunction(L,module_newindex_event);
	lua_rawset(L,-3);
}

/* Check if the object on the top has a module metatable
*/
TOLUA_API int tolua_ismodulemetatable (lua_State* L)
{
	int r = 0;
	if (lua_getmetatable(L,-1))
	{
		lua_pushstring(L,"__index");
		lua_rawget(L,-2);
		r = (lua_tocfunction(L,-1) == module_index_event);
		lua_pop(L,2);
	}
	return r;
}

/* Register class events
	* It expects the metatable on the top of the stack
*/
TOLUA_API void tolua_classevents (lua_State* L)
{
	lua_pushstring(L,"__index");
	lua_pushcfunction(L,class_index_event);
	lua_rawset(L,-3);
	lua_pushstring(L,"__newindex");
	lua_pushcfunction(L,class_newindex_event);
	lua_rawset(L,-3);

	lua_pushstring(L,"__add");
	lua_pushcfunction(L,class_add_event);
	lua_rawset(L,-3);
	lua_pushstring(L,"__sub");
	lua_pushcfunction(L,class_sub_event);
	lua_rawset(L,-3);
	lua_pushstring(L,"__mul");
	lua_pushcfunction(L,class_mul_event);
	lua_rawset(L,-3);
	lua_pushstring(L,"__div");
	lua_pushcfunction(L,class_div_event);
	lua_rawset(L,-3);

	lua_pushstring(L,"__lt");
	lua_pushcfunction(L,class_lt_event);
	lua_rawset(L,-3);
	lua_pushstring(L,"__le");
	lua_pushcfunction(L,class_le_event);
	lua_rawset(L,-3);
	lua_pushstring(L,"__eq");
	lua_pushcfunction(L,class_eq_event);
	lua_rawset(L,-3);

	lua_pushstring(L,"__call");
	lua_pushcfunction(L,class_call_event);
	lua_rawset(L,-3);

	lua_pushstring(L,"__gc");
	lua_pushstring(L, "tolua_gc_event");
	lua_rawget(L, LUA_REGISTRYINDEX);
	/*lua_pushcfunction(L,class_gc_event);*/
	lua_rawset(L,-3);
}

