#include "LuaInterpreter.h"
#ifdef NO_EMBEDDED_LUA
#include <lua.hpp>
#else
#include "src/lua.hpp"
#endif
#include "../Interface/Server.h"
#include "../common/data.h"
#include <assert.h>
#define MINIZ_NO_ZLIB_COMPATIBLE_NAMES
#include "../common/miniz.h"
#include "../stringtools.h"

#ifndef NO_EMBEDDED_LUA
extern "C" {
	LUAMOD_API int luaopen_os_custom(lua_State *L);
}
#endif

namespace
{
#include "lua/dkjson_lua.h"

	std::string get_lua(const unsigned char* data, size_t data_len)
	{
		size_t out_len;
		void* cdata = tinfl_decompress_mem_to_heap(data, data_len, &out_len, TINFL_FLAG_PARSE_ZLIB_HEADER | TINFL_FLAG_COMPUTE_ADLER32);
		if (cdata == NULL)
		{
			return std::string();
		}

		std::string ret(reinterpret_cast<char*>(cdata), reinterpret_cast<char*>(cdata) + out_len);
		mz_free(cdata);
		return ret;
	}
}

namespace
{
	int time_monotonic_ms(lua_State *L) {
		lua_pushnumber(L, static_cast<lua_Number>(Server->getTimeMS()));
		return 1;
	}

	int time_unix_seconds(lua_State *L) {
		lua_pushnumber(L, static_cast<lua_Number>(Server->getTimeSeconds()));
		return 1;
	}

	static const luaL_Reg lib_time[] = {
		{ "monotonic_ms",     time_monotonic_ms },
		{ "unix_seconds",      time_unix_seconds },
		{ NULL, NULL }
	};


	LUAMOD_API int luaopen_time(lua_State *L) {
		luaL_newlib(L, lib_time);
		return 1;
	}

	class ScopedLuaState
	{
		lua_State* state;
	public:
		ScopedLuaState(lua_State* state)
			: state(state)
		{}

		~ScopedLuaState() {
			lua_close(state);
		}
	};

#define LUA_SERIALIZE_STOP 100

	bool unserialize_val(lua_State* state, char t, CRData& data)
	{
		switch (t)
		{
		case LUA_TNIL:
		{
			lua_pushnil(state);
		}	break;
		case LUA_TBOOLEAN:
		{
			int64 b;
			if (!data.getVarInt(&b))
				return false;
			lua_pushboolean(state, static_cast<int>(b));
		}	break;
		case LUA_TNUMBER:
		{
			double num;
			if (!data.getDouble(&num))
				return false;
			lua_pushnumber(state, num);
		}	break;
		case LUA_TSTRING:
		{
			std::string str;
			if (!data.getStr2(&str))
				return false;
			lua_pushstring(state, str.c_str());
		}	break;
		default:
			return false;
		}

		return true;
	}

	bool unserialize_table(lua_State* state, CRData& data)
	{
		lua_newtable(state);

		char t;
		while (data.getChar(&t))
		{
			if (t == LUA_SERIALIZE_STOP)
				return true;

			if (!unserialize_val(state, t, data))
			{
				return false;
			}

			if (t == LUA_TNUMBER || t == LUA_TSTRING)
			{
				char t2;
				if (!data.getChar(&t2))
					return false;

				if (t2 == LUA_TTABLE)
				{
					if (!unserialize_table(state, data))
					{
						lua_rawset(state, -3);
						return false;
					}
				}
				else if (!unserialize_val(state, t2, data))
				{
					lua_pushnil(state);
					lua_rawset(state, -3);
					return false;
				}

				lua_rawset(state, -3);
			}
			else
			{
				lua_rawset(state, -2);
			}
		}

		return false;
	}

	bool unserialize_table(lua_State* state, const std::string& data_in)
	{
		CRData data(data_in.data(), data_in.size());
		return unserialize_table(state, data);
	}

	bool serialize_val(lua_State* state, int idx, CWData& data)
	{
		if (lua_isboolean(state, idx))
		{
			data.addChar(LUA_TBOOLEAN);
			data.addVarInt(lua_toboolean(state, idx));
		}
		else if (lua_isnil(state, idx))
		{
			data.addChar(LUA_TNIL);
		}
		else if (lua_isnumber(state, idx))
		{
			data.addChar(LUA_TNUMBER);
			data.addDouble(lua_tonumber(state, idx));
		}
		else if (lua_isstring(state, idx))
		{
			data.addChar(LUA_TSTRING);
			data.addString2(lua_tostring(state, idx));
		}		
		else
		{
			return false;
		}

		return true;
	}

	bool serialize_table(lua_State* state, CWData& data)
	{
		for (lua_pushnil(state); lua_next(state, -2) != 0; lua_pop(state, 1))
		{
			if (!serialize_val(state, -2, data))
				return false;

			if (lua_istable(state, -1))
			{
				data.addChar(LUA_TTABLE);
				if (!serialize_table(state, data))
					return false;
			}
			else if (!serialize_val(state, -1, data))
			{
				return false;
			}
		}

		data.addChar(LUA_SERIALIZE_STOP);

		return true;
	}

	std::string serialize_table(lua_State* state)
	{
		CWData data;
		if (!serialize_table(state, data))
			return std::string();

		return std::string(data.getDataPtr(), data.getDataSize());
	}

	int lua_write(lua_State *L, const void *p, size_t sz, void *ud)
	{
		try
		{
			std::string* output = reinterpret_cast<std::string*>(ud);

			output->insert(output->end(), reinterpret_cast<const char*>(p), reinterpret_cast<const char*>(p) + sz);
		}
		catch (...)
		{
			return 1;
		}

		return 0;
	}

	static const luaL_Reg loadedlibs[] = {
		{ "_G", luaopen_base },
		{ LUA_TABLIBNAME, luaopen_table },
		{ LUA_STRLIBNAME, luaopen_string },
		{ LUA_MATHLIBNAME, luaopen_math },
		{ LUA_UTF8LIBNAME, luaopen_utf8 },
#ifndef NO_EMBEDDED_LUA
		{ LUA_OSLIBNAME, luaopen_os_custom},
#endif
		{ "time", luaopen_time },
		{ NULL, NULL }
	};


	LUALIB_API void luaL_openlibs_custom(lua_State *L) {
		const luaL_Reg *lib;
		/* "require" functions from 'loadedlibs' and set results to global table */
		for (lib = loadedlibs; lib->func; lib++) {
			luaL_requiref(L, lib->name, lib->func, 1);
			lua_pop(L, 1);  /* remove lib */
		}
	}

	LUALIB_API void luaL_openlibs_all(lua_State *L) {
		luaL_openlibs(L);
		luaL_requiref(L, "time", luaopen_time, 1);
		lua_pop(L, 1);
	}

	ILuaInterpreter::SInterpreterFunctions* get_funcs(lua_State* L)
	{
		lua_getglobal(L, "_g_funcs");
		ILuaInterpreter::SInterpreterFunctions* ret 
			= reinterpret_cast<ILuaInterpreter::SInterpreterFunctions*>(lua_touserdata(L, -1));
		lua_pop(L, 1);

		if (ret == NULL)
		{
			luaL_error(L, "Internal functions not present");
		}

		return ret;
	}

	int l_mail(lua_State *L) {
		std::string to = luaL_checkstring(L, 1);
		std::string subj = luaL_checkstring(L, 2);
		std::string msg = luaL_checkstring(L, 3);

		ILuaInterpreter::SInterpreterFunctions* funcs = get_funcs(L);

		bool b = funcs->mail_func->mail(to, subj, msg);

		lua_pushboolean(L, b ? 1 : 0);
		return 1;
	}

	int l_request_url(lua_State *L)
	{
		std::string url = luaL_checkstring(L, 1);
		
		str_map params;
		if (lua_istable(L, 2))
		{
			lua_gettable(L, 2);
			for (lua_pushnil(L); lua_next(L, -2) != 0; lua_pop(L, 1))
			{
				std::string key = luaL_checkstring(L, -2);
				std::string val;
				if (lua_isboolean(L, -1))
				{
					if (lua_toboolean(L, -1))
						val = "1";
					else
						val = "0";
				}
				else if (lua_isnil(L, -1))
				{
					val = "null";
				}
				else if (lua_isstring(L, -1))
				{
					val = lua_tostring(L, -1);
				}
				else if (lua_isnumber(L, -1))
				{
					val = convert(lua_tonumber(L, -1));
				}
				else
				{
					const char* msg = lua_pushfstring(L, "%s expected, got %s", "boolean/number/string", luaL_typename(L, -1));
					luaL_argerror(L, -1, msg);
				}

				params[key] = val;
			}
		}

		ILuaInterpreter::SInterpreterFunctions* funcs = get_funcs(L);

		std::string errmsg;
		std::string ret;
		long http_code;
		bool b = funcs->url_func->requestUrl(url, params, ret, http_code, &errmsg);

		lua_pushboolean(L, b ? 1 : 0);
		lua_pushlstring(L, ret.c_str(), ret.size());
		lua_pushinteger(L, http_code);
		if (!errmsg.empty())
		{
			lua_pushlstring(L, errmsg.c_str(), errmsg.size());
			return 4;
		}
		return 3;
	}

	void set_param(lua_State* state, const ILuaInterpreter::Param& param)
	{
		if (param.tag == ILuaInterpreter::Param::PARAM_VEC)
		{
			lua_newtable(state);
			for (ILuaInterpreter::Param::params_map::const_iterator it = param.u.params->begin();
				it!=param.u.params->end();++it)
			{
				set_param(state, it->first);
				set_param(state, it->second);
				lua_rawset(state, -3);
			}
		}
		else if (param.tag == ILuaInterpreter::Param::PARAM_STR)
		{
			lua_pushstring(state, param.u.str->c_str());
		}
		else if (param.tag == ILuaInterpreter::Param::PARAM_NUM)
		{
			lua_pushnumber(state, param.u.num);
		}
		else if (param.tag == ILuaInterpreter::Param::PARAM_INT)
		{
			lua_pushinteger(state, param.u.i);
		}
		else if (param.tag == ILuaInterpreter::Param::PARAM_BOOL)
		{
			lua_pushboolean(state, param.u.b);
		}
		else
		{
			assert(false);
		}
	}

	int l_require(lua_State* state)
	{
		std::string name = luaL_checkstring(state, 1);
		std::string code;
		if (name == "dkjson")
		{
			code = get_lua(dkjson_lua_z, dkjson_lua_z_len);
			//code = getFile("luaplugin/lua/dkjson.lua");
		}
		if (code.empty())
		{
			return 0;
		}
		else
		{
			luaL_loadbuffer(state, code.data(), code.size(), name.c_str());
		}
		lua_call(state, 0, LUA_MULTRET);
		return 1;
	}

	int l_log(lua_State* state)
	{
		std::string msg = luaL_checkstring(state, 1);
		int loglevel = LL_INFO;
		if (lua_isinteger(state, 2))
		{
			loglevel = static_cast<int>(lua_tointeger(state, 2));
			if (loglevel<LL_DEBUG || loglevel>LL_ERROR)
				loglevel = LL_INFO;
		}
		Server->Log(msg, loglevel);
		return 0;
	}
}

std::string LuaInterpreter::compileScript(const std::string & script)
{
	lua_State* state = luaL_newstate();
	if (state == NULL)
	{
		return std::string();
	}

	ScopedLuaState scoped_state(state);

	int rc = luaL_loadbuffer(state, script.c_str(), script.size(), "script");
	if (rc) {
		Server->Log(std::string("Error loading lua script: ") + lua_tostring(state, -1), LL_ERROR);
		return std::string();
	}

	std::string ret;
	rc = lua_dump(state, lua_write, &ret, 0);
	if (rc)
	{
		Server->Log(std::string("Error dumping lua state"), LL_ERROR);
		return std::string();
	}

	return ret;
}

int64 LuaInterpreter::runScript(const std::string& script, const Param& params, int64& ret2,
	std::string& state_data, std::string& state_data_mem,
	std::string& global_data,
	std::string& global_data_mem, SInterpreterFunctions& funcs)
{
	ret2 = -1;

	lua_State* state = luaL_newstate();
	if (state == NULL)
	{
		return -1;
	}

	ScopedLuaState scoped_state(state);

	if (Server->getServerParameter("lua_sandbox") == "true")
	{
		luaL_openlibs_custom(state);
	}
	else
	{
		luaL_openlibs_all(state);
	}

	int rc = luaL_loadbuffer(state, script.c_str(), script.size(), "script");
	if (rc) {
		Server->Log(std::string("Error loading lua script: ") + lua_tostring(state, -1), LL_ERROR);
		return -1;
	}

	set_param(state, params);
	lua_setglobal(state, "params");

	lua_pushlightuserdata(state, const_cast<ILuaInterpreter::SInterpreterFunctions*>(&funcs));
	lua_setglobal(state, "_g_funcs");

	lua_pushcfunction(state, l_mail);
	lua_setglobal(state, "mail");
	lua_pushcfunction(state, l_require);
	lua_setglobal(state, "require");
	lua_pushcfunction(state, l_request_url);
	lua_setglobal(state, "request_url");

	lua_pushcfunction(state, l_log);
	lua_setglobal(state, "log");
	lua_pushinteger(state, LL_DEBUG);
	lua_setglobal(state,"LL_DEBUG");
	lua_pushinteger(state, LL_INFO);
	lua_setglobal(state, "LL_INFO");
	lua_pushinteger(state, LL_WARNING);
	lua_setglobal(state, "LL_WARNING");
	lua_pushinteger(state, LL_ERROR);
	lua_setglobal(state, "LL_ERROR");

	if (state_data.empty())
	{
		lua_newtable(state);
	}
	else if (!unserialize_table(state, state_data))
	{
		Server->Log("Error unserializing state data", LL_ERROR);
		assert(false);
	}

	lua_setglobal(state, "state");

	if (state_data_mem.empty())
	{
		lua_newtable(state);
	}
	else if (!unserialize_table(state, state_data_mem))
	{
		Server->Log("Error unserializing state data mem", LL_ERROR);
		assert(false);
	}

	lua_setglobal(state, "state_mem");

	if (global_data.empty())
	{
		lua_newtable(state);
	}
	else if (!unserialize_table(state, global_data))
	{
		Server->Log("Error unserializing global data", LL_ERROR);
		assert(false);
	}

	lua_setglobal(state, "global");

	if (global_data_mem.empty())
	{
		lua_newtable(state);
	}
	else if (!unserialize_table(state, global_data_mem))
	{
		Server->Log("Error unserializing global data mem", LL_ERROR);
		assert(false);
	}

	lua_setglobal(state, "global_mem");

	rc = lua_pcall(state, 0, LUA_MULTRET, 0);
	if (rc) {
		Server->Log(std::string("Error running lua script: ") + lua_tostring(state, -1), LL_ERROR);
		return -1;
	}

	if (lua_gettop(state) > 1)
	{
		ret2 = lua_tointeger(state, -1);
		lua_pop(state, 1);
	}
	int64 ret = lua_tointeger(state, -1);
	lua_pop(state, 1);

	lua_getglobal(state, "state");

	state_data = serialize_table(state);
	if (state_data.empty())
	{
		Server->Log("Error serializing state data", LL_ERROR);
		assert(false);
	}

	lua_getglobal(state, "state_mem");

	state_data_mem = serialize_table(state);
	if (state_data_mem.empty())
	{
		Server->Log("Error serializing state mem data", LL_ERROR);
		assert(false);
	}

	lua_getglobal(state, "global");

	global_data = serialize_table(state);
	if (global_data.empty())
	{
		Server->Log("Error serializing global data", LL_ERROR);
		assert(false);
	}

	lua_getglobal(state, "global_mem");

	global_data_mem = serialize_table(state);
	if (global_data_mem.empty())
	{
		Server->Log("Error serializing global mem data", LL_ERROR);
		assert(false);
	}
	
	return ret;
}
