
#include "file.h"

#include "bytearray.h"

#include "cfile/cfilesystem.h"

extern int cfread_lua_number(double* buf, CFILE* cfile);
namespace scripting {
namespace api {

cfile_h::cfile_h(CFILE* cfp) : _cfp(cfp) {}
cfile_h::~cfile_h()
{
	if (_cfp && cf_is_valid(_cfp.get())) {
		Warning(LOCATION, "Lua cfile handle of file %s has been left open!\nYou should close the file with the object's close() method explicitly.\n", cf_get_filename(_cfp.get()));
	}
}
bool cfile_h::isValid() const { return _cfp != nullptr; }
CFILE* cfile_h::get() const { return _cfp.get(); }
void cfile_h::close()
{
	_cfp = nullptr; // Automatically closes the handle
}

//**********HANDLE: File
ADE_OBJ(l_File, cfile_h, "file", "File handle");

ADE_FUNC(isValid, l_File, NULL, "Detects whether handle is valid", "boolean", "true if valid, false if handle is invalid, nil if a syntax/type error occurs")
{
	cfile_h* cfp = nullptr;
	if (!ade_get_args(L, "o", l_File.GetPtr(&cfp)))
		return ADE_RETURN_NIL;

	if (cfp == nullptr || !cfp->isValid())
		return ADE_RETURN_FALSE;

	return ADE_RETURN_TRUE;
}

ADE_FUNC(close, l_File, NULL, "Instantly closes file and invalidates all file handles", NULL, NULL)
{
	cfile_h* cfp = nullptr;
	if (!ade_get_args(L, "o", l_File.GetPtr(&cfp)))
		return ADE_RETURN_FALSE;

	if (cfp == nullptr || !cfp->isValid())
		return ADE_RETURN_FALSE;

	cfp->close();
	return ADE_RETURN_TRUE;
}

ADE_FUNC(flush, l_File, NULL, "Flushes file buffer to disk.", "boolean", "True for success, false on failure")
{
	cfile_h* cfp = nullptr;
	if (!ade_get_args(L, "o", l_File.GetPtr(&cfp)))
		return ADE_RETURN_FALSE;

	if (cfp == nullptr || !cfp->isValid())
		return ADE_RETURN_FALSE;

	//WMC - this looks reversed, yes, it's right. Look at cflush.
	int cf_result = cflush(cfp->get());
	return ade_set_args(L, "b", cf_result == 0);
}

ADE_FUNC(getName, l_File, nullptr, "Returns the name of the given file", "string", "Name of the file handle, or an empty string if it doesn't have one, or the handle is invalid")
{
	cfile_h* cfp = nullptr;
	if (!ade_get_args(L, "o", l_File.GetPtr(&cfp)))
		return ade_set_error(L, "s", "");

	if (cfp == nullptr || !cfp->isValid())
		return ade_set_error(L, "s", "");

	auto filename = cf_get_filename(cfp->get());
	if (filename != nullptr)
		return ade_set_args(L, "s", filename);
	else
		return ade_set_args(L, "s", "");
}

ADE_FUNC(getPath, l_File, nullptr, "Determines path of the given file", "string", "Path string of the file handle, or an empty string if it doesn't have one, or the handle is invalid")
{
	cfile_h* cfp = nullptr;
	if (!ade_get_args(L, "o", l_File.GetPtr(&cfp)))
		return ade_set_error(L, "s", "");

	if (cfp == nullptr || !cfp->isValid())
		return ade_set_error(L, "s", "");

	int id = cf_get_dir_type(cfp->get());
	if (id == CF_TYPE_ANY)
		return ade_set_args(L, "s", "<any path>");
	else if (Pathtypes[id].path != nullptr)
		return ade_set_args(L, "s", Pathtypes[id].path);
	else
		return ade_set_args(L, "s", "");
}

ADE_FUNC(read,
	l_File,
	"number|string, any...",
	"Reads part of or all of a file, depending on arguments passed. Based on basic Lua file:read function."
	"Returns nil when the end of the file is reached."
	"<br><ul><li>\"*n\" - Reads a number.</li>"
	"<li>\"*a\" - Reads the rest of the file and returns it as a string.</li>"
	"<li>\"*l\" - Reads a line. Skips the end of line markers.</li>"
	"<li>(number) - Reads given number of characters, then returns them as a string.</li></ul>",
	"string",
	"Requested data, or nil if the function fails")
{
	cfile_h* cfp = nullptr;
	if (!ade_get_args(L, "o", l_File.GetPtr(&cfp)))
		return ADE_RETURN_NIL;

	if (cfp == nullptr || !cfp->isValid())
		return ADE_RETURN_NIL;

	int i;
	int num_returned = 0;
	int type = LUA_TNONE;

	//WMC - Since we push stuff onto the stack, we must get
	//the original arguments NOW.
	int lastarg = lua_gettop(L);
	for (i = 2; i <= lastarg; i++)
	{
		type = lua_type(L, i);
		const char* fmt = nullptr;
		//int num = 0;
		if (type == LUA_TSTRING)
		{
			fmt = lua_tostring_nullsafe(L, i);
			if (!stricmp(fmt, "*n"))
			{
				double d = 0.0f;
				if (cfread_lua_number(&d, cfp->get()) == EOF)
					return ADE_RETURN_NIL;

				lua_pushnumber(L, d);
				num_returned++;
			}
			else if (!stricmp(fmt, "*a"))
			{
				int tell_res = cftell(cfp->get());
				if (tell_res < 0)
				{
					Error(LOCATION, "Critical error reading Lua file; could not cftell.");
				}
				int read_len = cfilelength(cfp->get()) - tell_res;

				char *buf = (char *)vm_malloc(read_len + 1);
				int final_len = cfread(buf, 1, read_len, cfp->get());
				buf[final_len] = '\0';

				lua_pushstring(L, buf);
				vm_free(buf);
				num_returned++;
			}
			else if (!stricmp(fmt, "*l"))
			{
				char buf[10240];
				size_t idx;
				if (cfgets(buf, (int)(sizeof(buf) / sizeof(char)), cfp->get()) == nullptr)
				{
					lua_pushnil(L);
				}
				else
				{
					// Strip all newlines so this works like the Lua original
					// http://www.lua.org/source/5.1/liolib.c.html#g_read
					// Note: we also strip carriage return in WMC's implementation
					for (idx = 0; idx < strlen(buf); idx++)
					{
						if (buf[idx] == '\n' || buf[idx] == '\r')
							buf[idx] = '\0';
					}

					lua_pushstring(L, buf);
				}
				num_returned++;
			}
		}
		if (type == LUA_TNUMBER || (type == LUA_TSTRING && strpbrk(fmt, "1234567890")))
		{
			int num = 0;
			if (type == LUA_TSTRING)
				num = atoi(fmt);
			else
				num = fl2i(lua_tonumber(L, i));

			if (num < 1)
			{
				if (cfeof(cfp->get()))
					lua_pushstring(L, "");
				else
					lua_pushnil(L);
			}

			char *buf = (char*)vm_malloc(num + 1);
			int total_read = cfread(buf, 1, num, cfp->get());
			if (total_read)
			{
				buf[total_read] = '\0';
				lua_pushstring(L, buf);
			}
			else
			{
				lua_pushnil(L);
			}
			vm_free(buf);
			num_returned++;
		}
		if (type != LUA_TNUMBER && type != LUA_TSTRING)
			LuaError(L, "Invalid argument passed to file:read");
	}

	return num_returned;
}

ADE_FUNC(seek, l_File, "[string Whence=\"cur\", number Offset=0]",
	"Changes position of file, or gets location."
	"Whence can be:"
	"<li>\"set\" - File start.</li>"
	"<li>\"cur\" - Current position in file.</li>"
	"<li>\"end\" - File end.</li></ul>",
	"number",
	"new offset, or false or nil on failure")
{
	const char* w = nullptr;
	int o = 0;

	cfile_h* cfp = nullptr;
	if (!ade_get_args(L, "o|si", l_File.GetPtr(&cfp), &w, &o))
		return ADE_RETURN_NIL;

	if (cfp == nullptr || !cfp->isValid())
		return ADE_RETURN_NIL;

	if (!(w == NULL || (!stricmp(w, "cur") && o != 0)))
	{
		int seek_type = CF_SEEK_CUR;
		if (!stricmp(w, "set"))
			seek_type = CF_SEEK_SET;
		else if (!stricmp(w, "cur"))
			seek_type = CF_SEEK_CUR;
		else if (!stricmp(w, "end"))
			seek_type = CF_SEEK_END;
		else
			LuaError(L, "Invalid where argument passed to seek() - '%s'", w);

		if (cfseek(cfp->get(), o, seek_type))
			return ADE_RETURN_FALSE;
	}

	int res = cftell(cfp->get());
	if (res >= 0)
		return ade_set_args(L, "i", res);
	else
		return ADE_RETURN_FALSE;
}

ADE_FUNC(write, l_File, "string|number, any...",
	"Writes a series of Lua strings or numbers to the current file.", "number", "Number of items successfully written.")
{
	cfile_h* cfp = nullptr;
	if (!ade_get_args(L, "o", l_File.GetPtr(&cfp)))
		return ade_set_error(L, "i", 0);

	if (cfp == nullptr || !cfp->isValid())
		return ade_set_error(L, "i", 0);

	int l_pos = 2;
	int type = LUA_TNONE;

	int num_successful = 0;
	while ((type = lua_type(L, l_pos)) != LUA_TNONE)
	{
		if (type == LUA_TSTRING)
		{
			auto s = lua_tostring_nullsafe(L, l_pos);
			if (cfwrite(s, (int)sizeof(char), (int)strlen(s), cfp->get()))
				num_successful++;
		}
		else if (type == LUA_TNUMBER)
		{
			double d = lua_tonumber(L, l_pos);
			char buf[32] = { 0 };
			sprintf(buf, LUA_NUMBER_FMT, d);
			if (cfwrite(buf, (int)sizeof(char), (int)strlen(buf), cfp->get()))
				num_successful++;
		}

		l_pos++;
	}

	return ade_set_args(L, "i", num_successful);
}

ADE_FUNC(writeBytes,
	l_File,
	"bytearray bytes",
	"Writes the specified data to the file",
	"number",
	"Number of bytes successfully written.")
{
	cfile_h* cfp = nullptr;
	bytearray_h* array = nullptr;
	if (!ade_get_args(L, "oo", l_File.GetPtr(&cfp), l_Bytearray.GetPtr(&array)))
		return ade_set_error(L, "i", 0);

	if (cfp == nullptr || !cfp->isValid())
		return ade_set_error(L, "i", 0);

	auto written = cfwrite(array->data().data(), 1, static_cast<int>(array->data().size()), cfp->get());

	return ade_set_args(L, "i", written);
}

ADE_FUNC(readBytes,
	l_File,
	nullptr,
	"Reads the entire contents of the file as a byte array.<br><b>Warning:</b> This may change the position inside the "
	"file.",
	"bytearray",
	"The bytes read from the file or empty array on error")
{
	cfile_h* cfp = nullptr;
	if (!ade_get_args(L, "o", l_File.GetPtr(&cfp)))
		return ade_set_error(L, "o", l_Bytearray.Set(bytearray_h()));

	if (cfp == nullptr || !cfp->isValid())
		return ade_set_error(L, "o", l_Bytearray.Set(bytearray_h()));

	cfseek(cfp->get(), 0, CF_SEEK_SET);
	SCP_vector<uint8_t> data;
	data.resize(cfilelength(cfp->get()));

	cfread(data.data(), 1, cfilelength(cfp->get()), cfp->get());

	return ade_set_error(L, "o", l_Bytearray.Set(bytearray_h(std::move(data))));
}

} // namespace api
} // namespace scripting
