#include <lua.hpp>
#include <stdint.h>
#include <time.h>
#include <assert.h>
#include <base/filesystem.h>
#include <bee/utility/unicode_win.h>

#ifdef WIN32
#	pragma warning(disable:4996)
#	include <windows.h>
#	include <direct.h>
#else
#	include <unistd.h>
#	include <sys/types.h> 
#	include <sys/stat.h>
#	include <sys/time.h>
#endif
			
namespace warcraft3::lua_engine::log {

enum loglevel {	 
	trace = 0,
	debug,
	info,
	warning,
	error,
	fatal
};

#ifdef WIN32
/*
* Number of micro-seconds between the beginning of the Windows epoch
* (Jan. 1, 1601) and the Unix epoch (Jan. 1, 1970). 
*
* This assumes all Win32 compilers have 64-bit support. 
*/
#if defined(_MSC_VER) || defined(_MSC_EXTENSIONS) || defined(__WATCOMC__)
#	define DELTA_EPOCH_IN_USEC 11644473600000000Ui64
#else
#	define DELTA_EPOCH_IN_USEC 11644473600000000ULL
#endif

static uint64_t log_filetime_to_unix_epoch(const FILETIME *ft)
{
	uint64_t res = (uint64_t)ft->dwHighDateTime << 32;

	res |= ft->dwLowDateTime;
	res /= 10; /* from 100 nano-sec periods to usec */
	res -= DELTA_EPOCH_IN_USEC; /* from Win epoch to Unix epoch */
	return (res);
}

static int gettimeofday(struct timeval *tv, void *)
{
	FILETIME ft;
	uint64_t tim;

	if (! tv) {
		//errno = EINVAL;
		return (-1);
	}
	GetSystemTimeAsFileTime(&ft);
	tim = log_filetime_to_unix_epoch(&ft);
	tv->tv_sec = (long)(tim / 1000000L);
	tv->tv_usec = (long)(tim % 1000000L);
	return (0);
}
#endif

static struct tm log_datenow(long* usec = 0)
{
	struct tm tm_now;
	struct timeval tv;
	gettimeofday(&tv, 0);
	time_t now = tv.tv_sec;
#ifdef WIN32
	localtime_s(&tm_now, &now);
#else
	localtime_r(&now, &tm_now);
#endif
	if (usec) *usec = tv.tv_usec;
	return tm_now;
}

static const char* log_filename(const char* path)
{
	static char filename[1024] = { 0 };
	static const char fmt[] = "%s\\log-%04d-%02d-%02d-%02d-%02d-%02d.log";
	struct tm t = log_datenow();
	sprintf(filename, fmt, path, t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec);
	return filename;
}

static void	log_print(lua_State* L, const char* msg)
{
	lua_pushstring(L, "__path");
	lua_rawget(L, lua_upvalueindex(1));
	size_t pathlen = 0;
	const wchar_t* path = (const wchar_t*)lua_tolstring(L, -1, &pathlen);
	if (path) {
		FILE* f = _wfopen(path, L"a");
		if (f)
		{
			fputs(msg, f);
			fclose(f);
		}
	}
	lua_pop(L, 1);
}

static const char* log_lvstr[] = {
	"trace",
	"debug",
	"info",
	"warn",
	"error",
	"fatal",
};

static const char *	log_timestr()
{
	static char str[64] = { 0 };
	long usec = 0;
	struct tm t = log_datenow(&usec);
	sprintf(str, "%04d-%02d-%02d %02d:%02d:%02d.%03d", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, usec / 1000);
	return str;
}

#define buffonstack(B)	((B)->b ! = (B)->init.b)

static int l_log(lua_State* L, int level)
{
	lua_pushstring(L, "__level");
	lua_rawget(L, lua_upvalueindex(1));
	if (level < lua_tointeger(L, -1))
	{
		return 0;
	}
	lua_pop(L, 1);

	luaL_Buffer b;
	luaL_buffinit(L, &b);
	luaL_addstring(&b, log_timestr());
	luaL_addstring(&b, " [");
	luaL_addstring(&b, log_lvstr[level]);
	luaL_addstring(&b, "]: ");
	assert(!buffonstack(&b));

	int n = lua_gettop(L);
	lua_getglobal(L, "tostring");
	for (int i = 1; i <= n; ++i)
	{
		lua_pushvalue(L, n + 1);
		lua_pushvalue(L, i);
		lua_call(L, 1, 1);
		size_t l;
		const char * s = lua_tolstring(L, -1, &l);
		if (buffonstack(&b)) lua_insert(L, -2);
		if (s == NULL) {
			s = "nil";
		}
		if (i>1) luaL_addchar(&b, '\t');
		luaL_addlstring(&b, s, l);
		lua_remove(L, n + 2);
	}

	luaL_addchar(&b, '\n');
	luaL_pushresult(&b);
	log_print(L, lua_tostring(L, -1));
	return 1;
}

static int l_logdebug(lua_State* L) { return l_log(L, debug); }
static int l_loginfo(lua_State* L) { return l_log(L, info); }
static int l_logwarning(lua_State* L) { return l_log(L, warning); }
static int l_logerror(lua_State* L) { return l_log(L, error); }
static int l_logfatal(lua_State* L) { return l_log(L, fatal); }
static int l_logtrace(lua_State* L) { return l_log(L, trace); }

static int l__index(lua_State* L)
{
	try {
		const char* key = lua_tostring(L, 2);
		if (strcmp(key, "path") == 0)
		{
			lua_pushstring(L, "__path");
			lua_rawget(L, lua_upvalueindex(1));
			size_t len = 0;
			const char* str = lua_tolstring(L, -1, &len);
			std::string utf8_str = bee::w2u(std::wstring_view((const wchar_t*)str, len / sizeof(wchar_t)));
			lua_pushlstring(L, utf8_str.c_str(), utf8_str.size());
			return 1;
		}
		else if (strcmp(key, "level") == 0)
		{
			lua_pushstring(L, "__level");
			lua_rawget(L, lua_upvalueindex(1));
			int level = lua_tointeger(L, 1);
			if (level >= 0 && level < sizeof(log_lvstr))
			{
				lua_pushstring(L, log_lvstr[level]);
				return 1;
			}
			return 0;
		}
	}
	catch (...) {}
	return 0;
}

static int l__newindex(lua_State* L)
{
	try {
		const char* key = lua_tostring(L, 2);
		if (strcmp(key, "path") == 0)
		{
			size_t pathlen = 0;
			const char* path = lua_tolstring(L, 3, &pathlen);
			std::wstring utf16_str = bee::u2w(std::string_view(path, pathlen));
			fs::create_directories(fs::path(utf16_str).parent_path());
			lua_pushstring(L, "__path");
			lua_pushlstring(L, (const char*)utf16_str.c_str(), (utf16_str.size() + 1) * sizeof(wchar_t));
			lua_rawset(L, lua_upvalueindex(1));
			return 0;
		}
		else if (strcmp(key, "level") == 0)
		{
			const char* level = lua_tostring(L, 3);
			for (int i = 0; i < sizeof(log_lvstr); ++i)
			{
				if (strcmp(log_lvstr[i], level) == 0)
				{
					lua_pushstring(L, "__level");
					lua_pushinteger(L, i);
					lua_rawset(L, lua_upvalueindex(1));
					return 0;
				}
			}
			return 0;
		}
	}
	catch (...) {}
	return 0;
}

int open(lua_State* L)
{
	static luaL_Reg func[] = {
		{ "debug", l_logdebug },
		{ "info", l_loginfo },
		{ "warn", l_logwarning },
		{ "error", l_logerror },
		{ "fatal", l_logfatal },
		{ "trace", l_logtrace },
		{ NULL, NULL },
	};
	lua_newtable(L);
	lua_pushvalue(L, -1);
	luaL_setfuncs(L, func, 1);

	static luaL_Reg mt[] = {
		{ "__index", l__index },
		{ "__newindex", l__newindex },
		{ NULL, NULL },
	};
	lua_newtable(L);
	lua_pushvalue(L, -2);
	luaL_setfuncs(L, mt, 1);
	lua_setmetatable(L, -2);

	const char* path = log_filename("log");
	lua_pushstring(L, path);
	lua_setfield(L, -2, "path");

	return 1;
}

}
