/**
 * Copyright (c) 2006-2020 LOVE Development Team
 *
 * This software is provided 'as-is', without any express or implied
 * warranty.  In no event will the authors be held liable for any damages
 * arising from the use of this software.
 *
 * Permission is granted to anyone to use this software for any purpose,
 * including commercial applications, and to alter it and redistribute it
 * freely, subject to the following restrictions:
 *
 * 1. The origin of this software must not be misrepresented; you must not
 *    claim that you wrote the original software. If you use this software
 *    in a product, an acknowledgment in the product documentation would be
 *    appreciated but is not required.
 * 2. Altered source versions must be plainly marked as such, and must not be
 *    misrepresented as being the original software.
 * 3. This notice may not be removed or altered from any source distribution.
 **/

// LOVE
#include "wrap_Mesh.h"
#include "wrap_Buffer.h"
#include "Texture.h"
#include "wrap_Texture.h"

// C++
#include <algorithm>

namespace love
{
namespace graphics
{

Mesh *luax_checkmesh(lua_State *L, int idx)
{
	return luax_checktype<Mesh>(L, idx);
}

int w_Mesh_setVertices(lua_State *L)
{
	Mesh *t = luax_checkmesh(L, 1);

	int vertstart = (int) luaL_optnumber(L, 3, 1) - 1;

	int vertcount = -1;
	if (!lua_isnoneornil(L, 4))
	{
		vertcount = (int) luaL_checknumber(L, 4);
		if (vertcount <= 0)
			return luaL_error(L, "Vertex count must be greater than 0.");
	}

	size_t stride = t->getVertexStride();
	size_t byteoffset = vertstart * stride;
	int totalverts = (int) t->getVertexCount();

	if (vertstart >= totalverts || vertstart < 0)
		return luaL_error(L, "Invalid vertex start index (must be between 1 and %d)", totalverts);

	if (luax_istype(L, 2, Data::type))
	{
		Data *d = luax_checktype<Data>(L, 2);

		vertcount = vertcount >= 0 ? vertcount : (totalverts - vertstart);
		if (vertstart + vertcount > totalverts)
			return luaL_error(L, "Too many vertices (expected at most %d, got %d)", totalverts - vertstart, vertcount);

		size_t datasize = std::min(d->getSize(), vertcount * stride);
		char *bytedata = (char *) t->getVertexData() + byteoffset;

		memcpy(bytedata, d->getData(), datasize);

		t->setVertexDataModified(byteoffset, datasize);
		t->flush();

		return 0;
	}

	luaL_checktype(L, 2, LUA_TTABLE);
	int tablelen = (int) luax_objlen(L, 2);

	vertcount = vertcount >= 0 ? std::min(vertcount, tablelen) : tablelen;
	if (vertstart + vertcount > totalverts)
		return luaL_error(L, "Too many vertices (expected at most %d, got %d)", totalverts - vertstart, vertcount);

	const std::vector<Buffer::DataMember> &vertexformat = t->getVertexFormat();

	int ncomponents = 0;
	for (const Buffer::DataMember &member : vertexformat)
		ncomponents += member.info.components;

	char *data = (char *) t->getVertexData() + byteoffset;

	for (int i = 0; i < vertcount; i++)
	{
		// get vertices[vertindex]
		lua_rawgeti(L, 2, i + 1);
		luaL_checktype(L, -1, LUA_TTABLE);

		// get vertices[vertindex][j]
		for (int j = 1; j <= ncomponents; j++)
			lua_rawgeti(L, -j, j);

		int idx = -ncomponents;

		for (const Buffer::DataMember &member : vertexformat)
		{
			// Fetch the values from Lua and store them in data buffer.
			luax_writebufferdata(L, idx, member.decl.format, data + member.offset);
			idx += member.info.components;
		}

		lua_pop(L, ncomponents + 1);

		data += stride;
	}

	t->setVertexDataModified(byteoffset, vertcount * stride);
	t->flush();

	return 0;
}

int w_Mesh_setVertex(lua_State *L)
{
	Mesh *t = luax_checkmesh(L, 1);
	size_t index = (size_t) luaL_checkinteger(L, 2) - 1;

	bool istable = lua_istable(L, 3);

	const std::vector<Buffer::DataMember> &vertexformat = t->getVertexFormat();

	char *data = nullptr;
	size_t offset = 0;
	luax_catchexcept(L, [&](){ data = (char *) t->checkVertexDataOffset(index, &offset); });

	int idx = istable ? 1 : 3;

	if (istable)
	{
		for (const Buffer::DataMember &member : vertexformat)
		{
			int components = member.info.components;

			for (int i = idx; i < idx + components; i++)
				lua_rawgeti(L, 3, i);

			// Fetch the values from Lua and store them in data buffer.
			luax_writebufferdata(L, -components, member.decl.format, data + member.offset);

			idx += components;
			lua_pop(L, components);
		}
	}
	else
	{
		for (const Buffer::DataMember &member : vertexformat)
		{
			// Fetch the values from Lua and store them in data buffer.
			luax_writebufferdata(L, idx, member.decl.format, data + member.offset);
			idx += member.info.components;
		}
	}

	t->setVertexDataModified(offset, t->getVertexStride());
	return 0;
}

int w_Mesh_getVertex(lua_State *L)
{
	Mesh *t = luax_checkmesh(L, 1);
	size_t index = (size_t) luaL_checkinteger(L, 2) - 1;

	const std::vector<Buffer::DataMember> &vertexformat = t->getVertexFormat();

	const char *data = nullptr;
	luax_catchexcept(L, [&](){ data = (const char *) t->checkVertexDataOffset(index, nullptr); });

	int n = 0;

	for (const Buffer::DataMember &member : vertexformat)
	{
		luax_readbufferdata(L, member.decl.format, data + member.offset);
		n += member.info.components;
	}

	return n;
}

int w_Mesh_setVertexAttribute(lua_State *L)
{
	Mesh *t = luax_checkmesh(L, 1);
	size_t vertindex = (size_t) luaL_checkinteger(L, 2) - 1;
	int attribindex = (int) luaL_checkinteger(L, 3) - 1;

	const auto &vertexformat = t->getVertexFormat();

	if (attribindex < 0 || attribindex >= (int) vertexformat.size())
		return luaL_error(L, "Invalid vertex attribute index: %d", attribindex + 1);

	const Buffer::DataMember &member = vertexformat[attribindex];

	char *data = nullptr;
	size_t offset = 0;
	luax_catchexcept(L, [&](){ data = (char *) t->checkVertexDataOffset(vertindex, &offset); });

	// Fetch the values from Lua and store them in the data buffer.
	luax_writebufferdata(L, 4, member.decl.format, data + member.offset);

	t->setVertexDataModified(offset + member.offset, member.size);
	return 0;
}

int w_Mesh_getVertexAttribute(lua_State *L)
{
	Mesh *t = luax_checkmesh(L, 1);
	size_t vertindex = (size_t) luaL_checkinteger(L, 2) - 1;
	int attribindex = (int) luaL_checkinteger(L, 3) - 1;

	const auto &vertexformat = t->getVertexFormat();

	if (attribindex < 0 || attribindex >= (int) vertexformat.size())
		return luaL_error(L, "Invalid vertex attribute index: %d", attribindex + 1);

	const Buffer::DataMember &member = vertexformat[attribindex];

	const char *data = nullptr;
	luax_catchexcept(L, [&](){ data = (const char *) t->checkVertexDataOffset(vertindex, nullptr); });

	luax_readbufferdata(L, member.decl.format, data + member.offset);
	return member.info.components;
}

int w_Mesh_getVertexCount(lua_State *L)
{
	Mesh *t = luax_checkmesh(L, 1);
	lua_pushinteger(L, t->getVertexCount());
	return 1;
}

int w_Mesh_getVertexFormat(lua_State *L)
{
	Mesh *t = luax_checkmesh(L, 1);

	const std::vector<Buffer::DataMember> &vertexformat = t->getVertexFormat();
	lua_createtable(L, (int) vertexformat.size(), 0);

	const char *tname = nullptr;

	for (size_t i = 0; i < vertexformat.size(); i++)
	{
		const auto &decl = vertexformat[i].decl;

		if (!getConstant(decl.format, tname))
			return luax_enumerror(L, "vertex attribute data type", getConstants(decl.format), tname);

		lua_createtable(L, 3, 0);

		lua_pushstring(L, decl.name.c_str());
		lua_rawseti(L, -2, 1);

		lua_pushstring(L, tname);
		lua_rawseti(L, -2, 2);

		// format[i] = {name, type}
		lua_rawseti(L, -2, (int) i + 1);
	}

	return 1;
}

int w_Mesh_setAttributeEnabled(lua_State *L)
{
	Mesh *t = luax_checkmesh(L, 1);
	const char *name = luaL_checkstring(L, 2);
	bool enable = luax_checkboolean(L, 3);
	luax_catchexcept(L, [&](){ t->setAttributeEnabled(name, enable); });
	return 0;
}

int w_Mesh_isAttributeEnabled(lua_State *L)
{
	Mesh *t = luax_checkmesh(L, 1);
	const char *name = luaL_checkstring(L, 2);
	bool enabled = false;
	luax_catchexcept(L, [&](){ enabled = t->isAttributeEnabled(name); });
	lua_pushboolean(L, enabled);
	return 1;
}

int w_Mesh_attachAttribute(lua_State *L)
{
	Mesh *t = luax_checkmesh(L, 1);
	const char *name = luaL_checkstring(L, 2);

	Buffer *buffer = nullptr;
	Mesh *mesh = nullptr;
	if (luax_istype(L, 3, Buffer::type))
	{
		buffer = luax_checktype<Buffer>(L, 3);
	}
	else
	{
		mesh = luax_checkmesh(L, 3);
		buffer = mesh->getVertexBuffer();
		if (buffer == nullptr)
			return luaL_error(L, "Mesh does not have its own vertex buffer.");
	}

	AttributeStep step = STEP_PER_VERTEX;
	const char *stepstr = lua_isnoneornil(L, 4) ? nullptr : luaL_checkstring(L, 4);
	if (stepstr != nullptr && !getConstant(stepstr, step))
		return luax_enumerror(L, "vertex attribute step", getConstants(step), stepstr);

	const char *attachname = luaL_optstring(L, 5, name);

	luax_catchexcept(L, [&](){ t->attachAttribute(name, buffer, mesh, attachname, step); });
	return 0;
}

int w_Mesh_detachAttribute(lua_State *L)
{
	Mesh *t = luax_checkmesh(L, 1);
	const char *name = luaL_checkstring(L, 2);
	bool success = false;
	luax_catchexcept(L, [&](){ success = t->detachAttribute(name); });
	luax_pushboolean(L, success);
	return 1;
}

int w_Mesh_getAttachedAttributes(lua_State *L)
{
	Mesh *t = luax_checkmesh(L, 1);
	const auto &attributes = t->getAttachedAttributes();

	lua_createtable(L, (int) attributes.size(), 0);

	for (int i = 0; i < (int) attributes.size(); i++)
	{
		const auto &attrib = attributes[i];

		lua_createtable(L, 4, 0);

		luax_pushstring(L, attrib.name);
		lua_rawseti(L, -1, 1);

		luax_pushtype(L, attrib.buffer.get());
		lua_rawseti(L, -1, 2);

		const char *stepstr = nullptr;
		if (!getConstant(attrib.step, stepstr))
			return luaL_error(L, "Invalid vertex attribute step.");
		lua_pushstring(L, stepstr);
		lua_rawseti(L, -1, 3);

		const Buffer::DataMember &member = attrib.buffer->getDataMember(attrib.indexInBuffer);
		luax_pushstring(L, member.decl.name);
		lua_rawseti(L, -1, 4);

		lua_rawseti(L, -1, i + 1);
	}

	return 1;
}

int w_Mesh_getVertexBuffer(lua_State *L)
{
	Mesh *t = luax_checkmesh(L, 1);
	luax_pushtype(L, t->getVertexBuffer());
	return 1;
}

int w_Mesh_flush(lua_State *L)
{
	Mesh *t = luax_checkmesh(L, 1);
	t->flush();
	return 0;
}

int w_Mesh_setVertexMap(lua_State *L)
{
	Mesh *t = luax_checkmesh(L, 1);

	if (lua_isnoneornil(L, 2))
	{
		// Disable the vertex map / index buffer.
		luax_catchexcept(L, [&](){ t->setVertexMap(); });
		return 0;
	}

	if (luax_istype(L, 2, Data::type))
	{
		Data *d = luax_totype<Data>(L, 2, Data::type);

		const char *indextypestr = luaL_checkstring(L, 3);
		IndexDataType indextype;
		if (!getConstant(indextypestr, indextype))
			return luax_enumerror(L, "index data type", getConstants(indextype), indextypestr);

		size_t datatypesize = getIndexDataSize(indextype);

		int indexcount = (int) luaL_optinteger(L, 4, d->getSize() / datatypesize);

		if (indexcount < 1 || indexcount * datatypesize > d->getSize())
			return luaL_error(L, "Invalid index count: %d", indexcount);

		luax_catchexcept(L, [&]() { t->setVertexMap(indextype, d->getData(), indexcount * datatypesize); });
		return 0;
	}

	bool is_table = lua_istable(L, 2);
	int nargs = is_table ? (int) luax_objlen(L, 2) : lua_gettop(L) - 1;

	std::vector<uint32> vertexmap;
	vertexmap.reserve(nargs);

	if (is_table)
	{
		for (int i = 0; i < nargs; i++)
		{
			lua_rawgeti(L, 2, i + 1);
			vertexmap.push_back(uint32(luaL_checkinteger(L, -1) - 1));
			lua_pop(L, 1);
		}
	}
	else
	{
		for (int i = 0; i < nargs; i++)
			vertexmap.push_back(uint32(luaL_checkinteger(L, i + 2) - 1));
	}

	luax_catchexcept(L, [&](){ t->setVertexMap(vertexmap); });
	return 0;
}

int w_Mesh_getVertexMap(lua_State *L)
{
	Mesh *t = luax_checkmesh(L, 1);

	std::vector<uint32> vertex_map;
	bool has_vertex_map = false;
	luax_catchexcept(L, [&](){ has_vertex_map = t->getVertexMap(vertex_map); });

	if (!has_vertex_map)
	{
		lua_pushnil(L);
		return 1;
	}

	int element_count = (int) vertex_map.size();

	lua_createtable(L, element_count, 0);

	for (int i = 0; i < element_count; i++)
	{
		lua_pushinteger(L, lua_Integer(vertex_map[i]) + 1);
		lua_rawseti(L, -2, i + 1);
	}

	return 1;
}

int w_Mesh_setIndexBuffer(lua_State *L)
{
	Mesh *t = luax_checkmesh(L, 1);
	Buffer *b = nullptr;
	if (!lua_isnoneornil(L, 2))
		b = luax_checkbuffer(L, 2);
	luax_catchexcept(L, [&]() { t->setIndexBuffer(b); });
	return 0;
}

int w_Mesh_getIndexBuffer(lua_State *L)
{
	Mesh *t = luax_checkmesh(L, 1);
	luax_pushtype(L, t->getIndexBuffer());
	return 1;
}

int w_Mesh_setTexture(lua_State *L)
{
	Mesh *t = luax_checkmesh(L, 1);

	if (lua_isnoneornil(L, 2))
		t->setTexture();
	else
	{
		Texture *tex = luax_checktexture(L, 2);
		luax_catchexcept(L, [&](){ t->setTexture(tex); });
	}

	return 0;
}

int w_Mesh_getTexture(lua_State *L)
{
	Mesh *t = luax_checkmesh(L, 1);
	Texture *tex = t->getTexture();

	if (tex == nullptr)
		return 0;

	luax_pushtype(L, tex);
	return 1;
}

int w_Mesh_setDrawMode(lua_State *L)
{
	Mesh *t = luax_checkmesh(L, 1);
	const char *str = luaL_checkstring(L, 2);
	PrimitiveType mode;

	if (!getConstant(str, mode))
		return luax_enumerror(L, "mesh draw mode", getConstants(mode), str);

	t->setDrawMode(mode);
	return 0;
}

int w_Mesh_getDrawMode(lua_State *L)
{
	Mesh *t = luax_checkmesh(L, 1);
	PrimitiveType mode = t->getDrawMode();
	const char *str;

	if (!getConstant(mode, str))
		return luaL_error(L, "Unknown mesh draw mode.");

	lua_pushstring(L, str);
	return 1;
}

int w_Mesh_setDrawRange(lua_State *L)
{
	Mesh *t = luax_checkmesh(L, 1);

	if (lua_isnoneornil(L, 2))
		t->setDrawRange();
	else
	{
		int start = (int) luaL_checkinteger(L, 2) - 1;
		int count = (int) luaL_checkinteger(L, 3);
		luax_catchexcept(L, [&](){ t->setDrawRange(start, count); });
	}

	return 0;
}

int w_Mesh_getDrawRange(lua_State *L)
{
	Mesh *t = luax_checkmesh(L, 1);

	int start = 0;
	int count = 1;
	if (!t->getDrawRange(start, count))
		return 0;

	lua_pushinteger(L, start + 1);
	lua_pushinteger(L, count);
	return 2;
}

static const luaL_Reg w_Mesh_functions[] =
{
	{ "setVertices", w_Mesh_setVertices },
	{ "setVertex", w_Mesh_setVertex },
	{ "getVertex", w_Mesh_getVertex },
	{ "setVertexAttribute", w_Mesh_setVertexAttribute },
	{ "getVertexAttribute", w_Mesh_getVertexAttribute },
	{ "getVertexCount", w_Mesh_getVertexCount },
	{ "getVertexFormat", w_Mesh_getVertexFormat },
	{ "setAttributeEnabled", w_Mesh_setAttributeEnabled },
	{ "isAttributeEnabled", w_Mesh_isAttributeEnabled },
	{ "attachAttribute", w_Mesh_attachAttribute },
	{ "detachAttribute", w_Mesh_detachAttribute },
	{ "getAttachedAttributes", w_Mesh_getAttachedAttributes },
	{ "getVertexBuffer", w_Mesh_getVertexBuffer },
	{ "flush", w_Mesh_flush },
	{ "setVertexMap", w_Mesh_setVertexMap },
	{ "getVertexMap", w_Mesh_getVertexMap },
	{ "setIndexBuffer", w_Mesh_setIndexBuffer },
	{ "getIndexBuffer", w_Mesh_getIndexBuffer },
	{ "setTexture", w_Mesh_setTexture },
	{ "getTexture", w_Mesh_getTexture },
	{ "setDrawMode", w_Mesh_setDrawMode },
	{ "getDrawMode", w_Mesh_getDrawMode },
	{ "setDrawRange", w_Mesh_setDrawRange },
	{ "getDrawRange", w_Mesh_getDrawRange },
	{ 0, 0 }
};

extern "C" int luaopen_mesh(lua_State *L)
{
	return luax_register_type(L, &Mesh::type, w_Mesh_functions, nullptr);
}

} // graphics
} // love
