//*****************************************************************************
//  luatable.h
//
//  Lua table access from C.  Use it just like you would in Lua with [] syntax.
//
//	File maintained by:
//		Young Tae Son (yson@ea.com)
//
//	(c) 2005 Electronic Arts Inc.
//*****************************************************************************

#pragma once

#include "luax/impl/luatype.h"
#include <EASTL/string.h>
#include <EASTL/variant.h>
#include <EASTL/vector.h>
#include <EAAssert/eaassert.h>

#define LUA_TFLOAT (LUA_TNUMBER + 101)

namespace Luax
{

template <typename Key = const char*>
struct FastLuaTable
{
	using TableValueContainer = eastl::variant<int, float, bool, eastl::string>;

	struct TableValue
	{
		TableValue(Key key, TableValueContainer&& value)
			: mKey(key)
			, mValue(eastl::move(value))
		{
		}

		Key	mKey;
		TableValueContainer	mValue;
	};

	struct TableItem
	{
		TableItem(FastLuaTable& table, Key key)
			: mTable(table)
			, mKey(key)
		{
		}

		template<typename T> void operator=(T val)
		{
			mTable.mValues.emplace_back(mKey, TableValueContainer(val));
		}

		FastLuaTable&	mTable;
		Key				mKey;
	};

	FastLuaTable()
	{
		constexpr uint32_t kReserveSize = 20;
		mValues.reserve(kReserveSize);
	}

	TableItem operator[](const Key& idx)
	{
		return TableItem(*this, idx);
	}

	eastl::vector<TableValue>	mValues;
};

inline void PushReturnToLua(lua_State* lstate, const FastLuaTable<>& arg)
{
	lua_createtable(lstate, 0, arg.mValues.size());
	int top = lua_gettop(lstate);

	for (const auto& it : arg.mValues)
	{
		PushReturnToLua(lstate, it.mKey);
		eastl::visit([&lstate](auto&& arg) { PushReturnToLua(lstate, arg); }, it.mValue);
		lua_settable(lstate, top);
	}
}

//> ********************************************************************************************
//> LuaTable
//> Used to read from a lua table object
//> ********************************************************************************************

//> LuaRef
//> This holds a reference to a string object so the string can be reused without being recreated
class LuaRef
{
public:
	LuaRef(lua_State* L, const char* str)
		: lua_state(L)
	{
		lua_pushstring(L, str);
		ref = luaL_ref(L, LUA_REGISTRYINDEX);
	}

	~LuaRef()
	{
		if (ref >= 0)
		{
			luaL_unref(lua_state, LUA_REGISTRYINDEX, ref);
			ref = -1;
		}
	}

	LuaRef(LuaRef&& b)
		: lua_state(b.lua_state)
		, ref(b.ref)
	{
		b.ref = -1;
	}

	// push the value from the ref onto the stack
	void Push() const
	{
		lua_rawgeti(lua_state, LUA_REGISTRYINDEX, ref);
	}

	LuaRef(const LuaRef&) = delete;
	LuaRef& operator=(const LuaRef&) = delete;

private:
	lua_State* lua_state = nullptr;
	int ref = -1;
};

class LuaTable
{
public:
	struct TableValue
	{
		TableValue(lua_State* state, int stackIndex, const char* key)
			: mLuaState(state)
		{
			// push the key on to the stack
			lua_pushstring(state, key);

			// lua replaces the top of the stack with the object referenced by the key
			lua_rawget(state, stackIndex);
		}

		TableValue(lua_State* state, int stackIndex, int key)
			: mLuaState(state)
		{
			// lua replaces the top of the stack with the object referenced by the key
			lua_rawgeti(state, stackIndex, key);
		}

		TableValue(lua_State* state, int stackIndex, const LuaRef& ref)
			: mLuaState(state)
		{
			// push the reference value onto the stack
			ref.Push();

			// lua replaces the stop of the stack with the object referenced by the key
			lua_rawget(state, stackIndex);
		}


		inline operator LuaTable() const
		{
			// no not pop the state for LuaTable since we need it on the stack
			const int index = lua_gettop(mLuaState);
			EA_ASSERT_FORMATTED(lua_istable(mLuaState, index) == 1, ("lua type is incorrect: %s", lua_typename(mLuaState, lua_type(mLuaState, index))));
			return LuaTable(mLuaState, index, true);
		}

		template <typename T>
		inline operator T() const
		{
			T ret = GetFromLuaType<T>(mLuaState, lua_gettop(mLuaState));
			lua_pop(mLuaState, 1);
			return ret;
		}

		lua_State* mLuaState = nullptr;
	};

	struct iterator
	{
		TableValue operator*() const
		{
			return TableValue(mL, mStackIndex, mKey);
		}

		iterator operator++()
		{
			mKey++;
			return *this;
		}

		bool operator==(const iterator& b) const
		{
			return mKey == b.mKey;
		}

		bool operator!=(const iterator& b) const
		{
			return mKey != b.mKey;
		}

		lua_State* mL = nullptr;
		int mStackIndex = 0;
		int mKey = 1;
	};

	LuaTable(lua_State* state, int index, bool popOnDestroy = false)
		: mLuaState(state)
		, mStackIndex(index)
		, mPopNeeded(popOnDestroy)
	{}

	~LuaTable()
	{
		if (mPopNeeded)
		{
			lua_pop(mLuaState, 1);
		}
	}

	LuaTable(LuaTable&& b)
		: mLuaState(b.mLuaState)
		, mStackIndex(b.mStackIndex)
		, mPopNeeded(b.mPopNeeded)
	{
		b.mPopNeeded = false;
	}

	LuaTable(const LuaTable&) = delete;
	LuaTable& operator=(const LuaTable&) = delete;
	
	// generic function to acquire the item at index int or string
    template<typename T>
    TableValue operator[](T&& idx)
    {
        return TableValue(mLuaState, mStackIndex, idx);
    }

	// specialized function to get a type R at index specified by type T
	template<typename R, typename T>
	R get(T&& idx)
	{
		return TableValue(mLuaState, mStackIndex, idx);
	}

	// debug function to describe the details of the table keys - does not print out the values
	eastl::string describe() const
	{
		eastl::string str;
		str.reserve(1024);

		const int stackSize = lua_gettop(mLuaState);
		lua_pushnil(mLuaState);
		while (lua_next(mLuaState, mStackIndex) != 0)
		{
			str.append(lua_typename(mLuaState, lua_type(mLuaState, -2)));
			str.append(" : ");
			str.append(lua_typename(mLuaState, lua_type(mLuaState, -1)));
			if (lua_isstring(mLuaState, -2) == 1)
			{
				if (lua_isnumber(mLuaState, -2) == 1)
				{
					str.append_sprintf(" (%d)", lua_tointeger(mLuaState, -2));
				}
				else
				{
					str.append(" (");
					str.append(lua_tostring(mLuaState, -2));
					str.append(")");
				}
			}
			//if (lua_istable(mLuaState, -1) == 1)
			//{
			//	str.append(" -->\n");
			//	LuaTable branch(mLuaState, -1);
			//	eastl::string branchStr = branch.describe();
			//	str.append(branchStr);
			//}
			str.append("\n");
			lua_pop(mLuaState, 1);
		}
		EA_ASSERT(stackSize == lua_gettop(mLuaState));
		return str;
	}

    int Size() const
    {
		return (int)lua_objlen(mLuaState, mStackIndex);
    }

	iterator begin() const
	{
		return { mLuaState, mStackIndex, 1 };
	}

	iterator end() const
	{
		return { mLuaState, mStackIndex, Size() + 1 };
	}

private:

    lua_State*	mLuaState = nullptr;
    int			mStackIndex = 0;
	bool		mPopNeeded = false;
};

template<>
inline LuaTable GetFromLuaType<LuaTable>(lua_State* l, int idx)
{
    return LuaTable(l, idx);
}

}
