/** @file luaencoder.h
*	LUA encoder/decoder templates
*	@author Chris Burns (cburns@ea.com)
*
*	UX
*	(c) 2012 Electronic Arts Inc.
*/

#ifndef EABASETYPESLUA_LUAENCODER_H_
#define EABASETYPESLUA_LUAENCODER_H_

#include "EABaseTypes/datatypes.h"

#include "EABase/eabase.h"
#include "EABaseTypesLua/abs_index.h"
#include "EABaseTypesLua/EABaseTypesLua.h"
#include "EABaseTypesLua/function_userdata.h"
#include "EABaseTypesLua/userdata_userdata.h"

#include "eaassert/eaassert.h"

#include "lua.h"
#include "lauxlib.h"

namespace EA
{
	namespace Lua
	{
		/*! @class LuaEncoder luaencoder.h	"ux_lua/luaencoder.h"
		*	@brief Encodes data from a Decoder into a lua_State
		*/
		class LuaEncoder
		{
		public:
		
			/*!
			*	LuaEncoder Constructor
			*	@param	L	The lua_State to encode data into
			*/
			LuaEncoder(lua_State* L);

			// Encoder template interface
			void Reset();

			void BeginArray();
			void EndArray() {}
			
			void BeginArrayValue(size_t index);
			void EndArrayValue(size_t index);

			void BeginObject();
			void EndObject() {}

			void BeginObjectValue(const char* key, size_t length);
			void EndObjectValue(const char* key, size_t length);

			void Boolean(bool boolean);
			void Number(EA::BaseTypeFloat number);
			void String(const char* str, size_t length);
			void Function(EA::Types::FunctionIn function);
			void UserData(EA::Types::UserDataIn userData);
			void Null();

		private:

			lua_State*	mL;
			int			mStackBase;
		};

		/*! 
		*	Function for determining if a given LUA value is array-like
		*	@param	L		The lua_State
		*	@param	index	The stack index of the value
		*	@return	bool	true if the value is array-like, false otherwise
		*	@note	A LUA value is array-like if it contains contiguous 1-based keys
		*/
		bool IsArray(lua_State* L, int index);

		/*!
		*	@struct LuaDecoderParams
		*	@brief A structure containing data on what to decode from a lua_State
		*	@param	L	The lua_State
		*	@param	n	The number of values to decode from the lua_State
		*/
		struct LuaDecoderParams
		{
			LuaDecoderParams(lua_State* L, size_t n):
				mL(L),
				mN(n)
			{
			}

			lua_State*		mL;
			size_t			mN;
		};

		/*! @class LuaDecoder luaencoder.h	"ux_lua/luaencoder.h"
		*	@brief template class for a LuaDecoder
		*	@tparam	Encoder	The encoder type
		*/
		template<typename Encoder>
		class LuaDecoder
		{
		public:

			// Decoder template interface
			static void Decode(void* e, void* p)
			{
				Encoder* encoder = static_cast<Encoder*>(e);
				LuaDecoderParams* params = static_cast<LuaDecoderParams*>(p);
			
				int n = static_cast<int>(params->mN);

				while(n--)
				{
					Decode(encoder, params->mL, - 1 - n);
				}
			}

		private:

			static void DecodeArray(Encoder* encoder, lua_State* L, int index)
			{
				lua_checkstack(L, 1);

				encoder->BeginArray();

				int arrayIndex = 0;

				bool condition = true;

				// iterate over the indices
				do
				{
					arrayIndex++;

					lua_pushinteger(L, arrayIndex);

					lua_gettable(L, index);

					condition = !lua_isnil(L, -1);

					if(condition)
					{
						encoder->BeginArrayValue(static_cast<size_t>(index - 1));

						Decode(encoder, L, -1);

						encoder->EndArrayValue(static_cast<size_t>(index - 1));
					}

					// pop the value off the stack
					lua_pop(L, 1);
				}
				while(condition);

#if !defined(EA_FINAL)
				int numValues = 0;

				lua_pushnil(L);
				while(lua_next(L, index) != 0)
				{
					numValues++;
					lua_pop(L, 1);
				}

				int arrayIndices = arrayIndex - 1;
				EA_UNUSED(arrayIndices);
			
				EA_ASSERT_FORMATTED(arrayIndices == numValues, ("LUA array has %d contiguous array indices, but %d values", arrayIndices, numValues));
#endif

				encoder->EndArray();
			}

			static void DecodeObject(Encoder* encoder, lua_State* L, int index)
			{
				lua_checkstack(L, 2);

				encoder->BeginObject();

				lua_pushnil(L);

				// iterate over the keys
				while(lua_next(L, index) != 0)
				{
					// if the key is the string, we can deal with it
					// otherwise, we must ignore it
					if(lua_type(L, -2) == LUA_TSTRING)
					{
						size_t length;
						const char* str = lua_tolstring(L, -2, &length);

						encoder->BeginObjectValue(str, length);

						Decode(encoder, L, -1);

						encoder->EndObjectValue(str, length);
					}

					// pop the value off the stack, we need the key to iterate
					lua_pop(L, 1);
				}

				// lua_next should consume the last key

				encoder->EndObject();
			}

			static void DecodeTable(Encoder* encoder, lua_State* L, int index)
			{
				if(IsArray(L, index))
				{
					DecodeArray(encoder, L, index);
				}
				else
				{
					DecodeObject(encoder, L, index);
				}
			}

			static void DecodeFunction(Encoder* encoder, lua_State* L, int index)
			{
				EA::Types::FunctionRef function = EA::Lua::CreateFunction(L, index);
				encoder->Function(function);
			}

			static void DecodeUserData(Encoder* encoder, lua_State* L, int index)
			{
				void* userdata = lua_touserdata(L, index);
				if (userdata != NULL)
				{
					if (lua_getmetatable(L, index))
					{
						lua_getfield(L, LUA_REGISTRYINDEX, "Function");
						if (lua_rawequal(L, -1, -2))
						{
							encoder->Function(EA::Types::FunctionIn(static_cast<EA::Lua::function_userdata*>(userdata)->mFunc));
						}
						else
						{
							lua_pop(L, 1);
							lua_getfield(L, LUA_REGISTRYINDEX, "UserData");

							if(lua_rawequal(L, -1, -2))
							{
								encoder->UserData(EA::Types::UserDataIn(static_cast<EA::Lua::userdata_userdata*>(userdata)->mUData));
							}
						}

						lua_pop(L, 2);
					}
					else
					{
						lua_pop(L, 1);
						encoder->Null();
					}
				}
				else
				{
					encoder->Null();
				}
			}

			static void Decode(Encoder* encoder, lua_State* L, int index)
			{
				// convert the index to an absolute index
				index = abs_index(L, index);

				int type = lua_type(L, index);

				switch(type)
				{
					case LUA_TNIL:
						encoder->Null();
						break;
					case LUA_TNUMBER:
						encoder->Number(static_cast<EA::BaseTypeFloat>(lua_tonumber(L, index)));
						break;
					case LUA_TBOOLEAN:
						encoder->Boolean(lua_toboolean(L, index) != 0);
						break;
					case LUA_TSTRING:
						{
							size_t length;
							const char* str = lua_tolstring(L, index, &length);
							encoder->String(str, length);
						}
						break;
					case LUA_TTABLE:
						DecodeTable(encoder, L, index);
						break;
					case LUA_TFUNCTION:
						DecodeFunction(encoder, L, index);
						break;
					case LUA_TUSERDATA:
						DecodeUserData(encoder, L, index);
						break;
					default:
						EA_FAIL_FORMATTED(("Cannot decode unknown type %d", type));
						break;
				}
			}
		};
	}
}

#endif /* EABASETYPESLUA_LUAENCODER_H_ */
