/* -----------------------------------------------------------------------------
	GSFramework
	Copyright 2001-2013 Emmanuel Julien. All Rights Reserved.
----------------------------------------------------------------------------- */


		#include "import_obj/import_obj.h"
		#include "scene3d/scene.h"
		#include "scene3d/mobject.h"
		#include "core/graphic_resource_factory.h"
		#include "core/geometry.h"
		#include "metafile/nml_object.h"
		#include "ascii/parser.h"
		#include "filesystem/filesystem.h"
		#include "filesystem/io_handle.h"
		#include "platform.h"

		using namespace GS;
		using namespace GS::Core;
		using namespace GS::S3D;
		using namespace GS::AsciiParser;


//------------------------------------------------------------------------------
struct	ObjPoly
{
	List <int>	vtx, uv, nrm;
	uint		mat;

	ObjPoly() : mat(0) {}
};
struct	ObjGroup
{
	String		name;
	List <ObjPoly *> pol_list;

	void		Free()
	{	ListDeleteAllPtr(ObjPoly *, pol_list)	}
	~ObjGroup()
	{	Free();	}
};
struct	ObjMtl
{
	String		name;
	String		path;
};
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
static	void		ExportMaterialStage(NML::Tag *mtag, const String &texture, const char *channel, int index)
{
	if	(texture.IsEmpty())
		return;

	if	(NML::Tag *ttag = mtag->AddChild("TextureStage"))
	{
		ttag->AddChild("Active");
		ttag->AddChild("Texture", texture);
		ttag->AddChild("Channel", channel);
		ttag->AddChild("Index", index);
	}
}
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
bool				OBJImporter::LoadMaterialLibrary(List <ObjMtl *> &mtl_list, const char *uri)
{
	Array <char> obj;
	if	(!Platform::Get().io->FileLoad(uri, obj))
		__ERR__(__LOG_E__ << "OBJ material library file '" << uri << "' not found.\n", false)

	// Now parse.
	const char *pobj = &obj[0], *eobj = pobj + obj.GetSize();

	ObjMtl *current_mtl = NULL;
	String map_Ka, map_Kd, map_Ks, map_Ke;

	forever
	{
		String word(pobj, SkipEntry(pobj, eobj));

		// Export material to file.
		if	((word == "newmtl") || (pobj >= eobj))
			if	(current_mtl)
			{
				current_mtl->path = String::Format("%s/%s.nmm", config->base_path.c_str(), current_mtl->name.c_str());

				// Only export material if it does not exist.
				if	(!Platform::Get().io->Exists(current_mtl->path))
				{
					String name = Platform::Get().io->StripRootPath(current_mtl->path);

					NML::File file;
					if	(NML::Tag *mtag = file.AddRoot("Material"))
					{
						mtag->AddChild("Id", name);

						ExportMaterialStage(mtag, map_Kd, "Diffuse", 0);
						ExportMaterialStage(mtag, map_Ka, "Opacity", 1);
						ExportMaterialStage(mtag, map_Ks, "Specular", 2);
						ExportMaterialStage(mtag, map_Ke, "Emissive", 3);

						if	(NML::Tag *rtag = mtag->AddChild("RenderMask"))
							rtag->AddChild("NormalMapTangent");
					}

					NML::Parser::Save(current_mtl->path, file);
				}
			}

		if	(pobj >= eobj)
			break;

		// Create new material.
		if	(word == "newmtl")
		{
			// Create new material.
			mtl_list.Add(current_mtl = new ObjMtl);

			pobj = NextEntry(pobj + 1, eobj);
			const char *eon = SkipEntry(pobj, eobj);

			current_mtl->name.Set(pobj, eon);
			pobj = NextEntry(eon, eobj, true);
		}
		else	if	(current_mtl)
		{
			//------------------------------------------------------------------
			#define	__ReadObjMaterialMap(_map) \
			{ \
				pobj = NextEntry(pobj + 1, eobj); \
				const char	*eon = RunToEOL(pobj, eobj); \
				_map.Set(pobj, eon); \
				pobj = NextEntry(eon, eobj, true); \
			}
			//------------------------------------------------------------------

					if	(word == "map_Ka")
				__ReadObjMaterialMap(map_Ka)
			else	if	(word == "map_Ks")
				__ReadObjMaterialMap(map_Ks)
			else	if	(word == "map_Kd")
				__ReadObjMaterialMap(map_Kd)
			else	if	(word == "map_Ke")
				__ReadObjMaterialMap(map_Ke)
			else
				pobj = NextEntry(RunToEOL(pobj, eobj), eobj);
		}
		else
			pobj = NextEntry(RunToEOL(pobj, eobj), eobj);
	}
	return true;
}
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
bool				OBJImporter::TestImport(const char *uri)
{
	String ext = String::FileGetExtension(uri).Lower();
	return ext == "obj";
}
bool				OBJImporter::ImportScene(Scene *scene, const char *uri, const Config &_config, Group **)
{
	Array <char> obj;
	if	(!Platform::Get().io->FileLoad(uri, obj))
		__ERR__(__LOG_E__ << "OBJ file '" << uri << "' not found.\n", false)

	// Now parse.
	config = &_config;
	if	(config->event_handler)
		config->event_handler->OpenLoad();

	const char *pobj = &obj[0], *eobj = pobj + obj.GetSize();

	//--------------------------------------------------------------------------
	{
		bool has_uv = false, has_vnm = false;
		AutoList <Vector4 *> vtx_list, vnm_list;
		AutoList <Vector2 *> uv_list;

		AutoList <ObjGroup *> group_list;
		AutoList <ObjMtl *> mat_list;

		ObjGroup *current_group = new ObjGroup;
		current_group->name = "default";
		group_list.Add(current_group);

		int current_mat = 0;

		while (pobj < eobj)
		{
			String word(pobj, SkipEntry(pobj, eobj));

			if	(config->event_handler)
				config->event_handler->LoadProgress(String::Format("Parsing tag '%s'...", word.c_str()), (float)(pobj - &obj[0]) / obj.GetSize());

			// Declare material library.
			if	(word == "mtllib")
			{
				pobj = NextEntry(pobj + 1, eobj);
				const char	*eon = RunToEOL(pobj, eobj);

				String lib_path = String(pobj, eon);
				if	(!lib_path.IsAbsolutePath())
					lib_path = String(uri).GetFilePath() + "/" + lib_path;

				LoadMaterialLibrary(mat_list, lib_path);
				pobj = NextEntry(eon, eobj, true);
			}

			// Use material.
			else	if	(word == "usemtl")
			{
				pobj = NextEntry(pobj + 1, eobj);
				const char *eon = SkipEntry(pobj, eobj);
				String name(pobj, eon);

				// Locate in library.
				current_mat = 0;
				ListForeachPtr(ObjMtl *, mat, mat_list)
				{
					if	(mat->name == name)
						break;
					current_mat++;
				}

				pobj = NextEntry(eon, eobj, true);
			}

			// Append vertex.
			else	if	(word == "v")
			{
				// Parse vertex.
				pobj = NextEntry(pobj, eobj);

				float x = 0, y = 0, z = 0;

				x = String::atof(pobj, eobj); pobj = NextEntry(pobj, eobj, true);
				y = String::atof(pobj, eobj); pobj = NextEntry(pobj, eobj, true);
				z = String::atof(pobj, eobj); pobj = NextEntry(pobj, eobj, true);
				
				vtx_list.Add(new Vector4(x * config->scale, y * config->scale, -z * config->scale));
			}

			// Append vertex normal.
			else	if	(word == "vn")
			{
				pobj = NextEntry(pobj, eobj);

				float x = 0, y = 0, z = 0;

				x = String::atof(pobj, eobj); pobj = NextEntry(pobj, eobj, true);
				y = String::atof(pobj, eobj); pobj = NextEntry(pobj, eobj, true);
				z = String::atof(pobj, eobj); pobj = NextEntry(pobj, eobj, true);

//			vnm_list.Add(new nVector(-y, -z, -x));
				vnm_list.Add(new Vector4(x, y, -z));
			}

			// Append UV.
			else	if	(word == "vt")
			{
				pobj = NextEntry(pobj, eobj);

				float u = 0, v = 0;

				u = String::atof(pobj, eobj); pobj = NextEntry(pobj, eobj, true);
				v = String::atof(pobj, eobj); pobj = NextEntry(pobj, eobj, true);

				uv_list.Add(new Vector2(u, 1.f - v));
			}

			// Declare group.
			else	if	(word == "g")
			{
				group_list.Add(current_group = new ObjGroup);

				// Check for named group.
				const char *eol = RunToEOL(pobj, eobj);
				pobj = NextEntry(pobj + 1, eobj);

				if	(pobj <= eol)
				{
					const char *eon = SkipEntry(pobj, eobj);
					current_group->name.Set(pobj, eon);
					pobj = NextEntry(eon, eobj, true);
				}
				else	// no name
					pobj = NextEntry(eol, eobj, true);
			}

			// Build group polygons.
			else	if	(word == "f")
			{
				pobj = NextEntry(pobj, eobj);

				ObjPoly *pol = new ObjPoly;
				pol->mat = current_mat;

				const char *eol = RunToEOL(pobj, eobj);
				while (pobj < eol)
				{
					pol->vtx.Add(String::atoi(pobj));			// Index.
					pobj = NextEntry(pobj, eobj, true);

					if	(pobj[0] == '/')
					{
						pobj++;
						if	(pobj[0] != '/')
						{
							has_uv = true;
							pol->uv.Add(String::atoi(pobj));	// UV index.
							pobj = NextEntry(pobj, eobj, true);
						}
					}

					if	(pobj[0] == '/')
					{
						pobj++;
						has_vnm = true;
						pol->nrm.Add(String::atoi(pobj));		// Normal index.
						pobj = NextEntry(pobj, eobj, true);
					}
				}

				if	(pol->vtx.GetCount() < 3)
					delete pol;
				else
					current_group->pol_list.Add(pol);
			}
			else
				pobj = NextEntry(RunToEOL(pobj, eobj), eobj);
		}

		obj.Free();

		//--------------------------------------------------------------------------

		// Get flat UV array.
		Array <Vector2> flat_uv;
		if	(has_uv && flat_uv.Allocate(uv_list.GetCount()))
		{
			Vector2 *pflat_uv = flat_uv;
			ListForeachPtr(Vector2 *, uv, uv_list)
				*pflat_uv++ = *uv;
		}

		// Get flat vertex normal array.
		Array <Vector4> flat_vnm;
		if	(has_vnm && flat_vnm.Allocate(vnm_list.GetCount()))
		{
			Vector4 *pflat_vnm = flat_vnm;
			ListForeachPtr(Vector4 *, vnm, vnm_list)
				*pflat_vnm++ = *vnm;
		}

		//--------------------------------------------------------------------------

		Array <int> vtx_map(vtx_list.GetCount()),
					mat_map(mat_list.GetCount());

		//--------------------------------------------------------------------------

		// Build scene.
		int n_pg = 0;
		ListForeachPtr(ObjGroup *, group, group_list)
		{
			++n_pg;
			if	(!vtx_list.GetCount() || !group->pol_list.GetCount())
				continue;

			if	(config->event_handler)
				config->event_handler->LoadProgress(String::Format("Importing group '%s'...", group->name.toUtf8()), (float)(n_pg - 1) / group_list.GetCount());

			// Create geometry.
			Geometry *geo = new Geometry;

			// Remap vertices.
			for	(uint n = 0; n < vtx_map.GetCount(); ++n)
				vtx_map[n] = -1;

			// Total vertex count.
			uint vtx_count = 0;
			ListForeachPtr(ObjPoly *, p, group->pol_list)
				ListForeachPtr(int, i, p->vtx)
					if	(vtx_map[i - 1] == -1)
						vtx_map[i - 1] = vtx_count++;

			// Transfer vertices.
			if	(geo->vtx.Allocate(vtx_count))
			{
				for	(uint i = 0; i < vtx_list.GetCount(); ++i)
					if	(vtx_map[i] != -1)
						geo->vtx[vtx_map[i]] = *vtx_list[i];
			}
			else
				__LOG_E__ << "Failed to allocate " << geo->vtx.GetCount() << " vertices.\n";

			// Transfer topology and attributes.
			if	(geo->pol.Allocate(group->pol_list.GetCount()))
			{
				// Compute bind index count.
				uint binding_count = 0;
				ListForeachPtr(ObjPoly *, p, group->pol_list)
					binding_count += p->vtx.GetCount();

				if	(geo->binding.Allocate(binding_count))
				{
					uint *pbind = &geo->binding[0];

					// Allocate attributes.
					Vector2 *puv = NULL;
					if	(has_uv)
					{
						geo->uv[0].Allocate(binding_count);
						puv = &geo->uv[0][0];
					}

					Vector4 *pvnm = NULL;
					if	(has_vnm)
					{
						geo->vtx_normal.Allocate(binding_count);
						pvnm = &geo->vtx_normal[0];
					}

					// Transfer data.
					uint cpol = 0;
					ListForeachPtr(ObjPoly *, p, group->pol_list)
					{
						Polygon *pol = &geo->pol[cpol++];

						pol->vtx_count = (ushort)p->vtx.GetCount();
						pol->binding = pbind;
						pol->material = 0;

						for (int i = p->vtx.GetCount(); i > 0; --i)
						{
							int	idx = p->vtx[i - 1] - 1;
							if	(idx > (int)vtx_map.GetCount())
								idx = 0;
							*pbind++ = vtx_map[idx];
						}

						if	(puv)
							for (int i = p->uv.GetCount(); i > 0; --i)
								*puv++ = flat_uv[p->uv[i - 1] - 1];

						if	(pvnm)
							for (int i = p->nrm.GetCount(); i > 0; --i)
								*pvnm++ = flat_vnm[p->nrm[i - 1] - 1];
					}
				}
				else
					__LOG_E__ << "Failed to allocate " << geo->binding.GetCount() << " polygon binding entries.\n";

				// Remap and assign materials.
				for	(uint i = 0; i < mat_list.GetCount(); ++i)
					mat_map[i] = -1;

				int mat_count = 0;
				ListForeachPtr(ObjPoly *, p, group->pol_list)
					if	(p->mat < mat_map.GetCount())
						if	(mat_map[p->mat] == -1)
							mat_map[p->mat] = mat_count++;

				uint cpol = 0;
				ListForeachPtr(ObjPoly *, p, group->pol_list)
					geo->pol[cpol++].material = (ushort)(p->mat < mat_map.GetCount() ? mat_map[p->mat] : 0);

				// Load materials.
				if	(mat_list.GetCount())
				{
					geo->material_table.Allocate(mat_count);
					for	(uint i = 0; i < mat_list.GetCount(); ++i)
						if	(mat_map[i] != -1)
							geo->material_table[mat_map[i]].name = mat_list[i]->path;
				}
				else
					geo->material_table.Allocate(1);

				// Vertex normal.
				if	(!has_vnm)
					geo->ComputeVertexNormal(true);

				// Save geometry.
				geo->name = String::Format("%s/%s.nmg", config->base_path.c_str(), group->name.toUtf8());
				NML::SaveToFile(*geo, geo->name);

				geo->name = Platform::Get().io->StripRootPath(geo->name);

				// Add to scene.
				MObject *object = new MObject;
				object->name = group->name;
				scene->AddItem(object, true);
				object->geometry = geo->name;

				// Ease up memory.
				group->Free();
			}
			else
				__LOG_E__ << "Failed to allocate " << geo->pol.GetCount() << " polygons.\n";
		}
	}
	//--------------------------------------------------------------------------

	// Save scene.
	scene->name = String::Format("%s/scene.nms", config->base_path.c_str());
	NML::SaveToFile(*scene, scene->name);
	scene->name = Platform::Get().io->StripRootPath(scene->name);

	if	(config->event_handler)
		config->event_handler->EndLoad();
	return true;
}
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
Geometry			*OBJImporter::ImportGeometry(const char *uri, IResourceFactoryEvent *event_handler)
{
	if	(!uri)
		__ERR__(__LOG_E__ << "No URI to import.\n", NULL)

	AutoPtr <Geometry> geo(new Geometry);

	// Load file in memory.
	Array <char> obj;
	if	(!Platform::Get().io->FileLoad(uri, obj))
		__ERR__(__LOG_E__ << "File '" << uri << "' not found.\n", NULL)

	// Now parse.
	List <Vector4 *> vtx_list;
	List <Vector2 *> uv_list;
	List <ObjPoly *> pol_list;

	bool has_uv = false;
	const char	*pobj = &obj[0], *eobj = pobj + obj.GetSize();

	while (pobj < eobj)
	{
		String word(pobj, SkipEntry(pobj, eobj));

		if	(word == "v")
		{
			pobj = NextEntry(pobj, eobj);

			float x = 0, y = 0, z = 0;

			x = String::atof(pobj, eobj);
			pobj = NextEntry(pobj, eobj, true);
			y = String::atof(pobj, eobj);
			pobj = NextEntry(pobj, eobj, true);
			z = -String::atof(pobj, eobj);
			pobj = NextEntry(pobj, eobj, true);
			
			vtx_list.Add(new Vector4(x, y, z));
		}
		else	if	(word == "vt")
		{
			pobj = NextEntry(pobj, eobj);

			float u = 0, v = 0;

			u = String::atof(pobj, eobj);
			pobj = NextEntry(pobj, eobj, true);
			v = String::atof(pobj, eobj);
			pobj = NextEntry(pobj, eobj, true);

			uv_list.Add(new Vector2(u, v));
		}
		else	if	(word == "f")
		{
			pobj = NextEntry(pobj, eobj);

			ObjPoly *pol = new ObjPoly;
			pol_list.Add(pol);

			const char *eol = RunToEOL(pobj, eobj);
			while (pobj < eol)
			{
				pol->vtx.Add(String::atoi(pobj));			// Index.
				pobj = NextEntry(pobj, eobj, true);

				if	(pobj[0] == '/')
				{
					pobj++;
					if	(pobj[0] != '/')
					{
						has_uv = true;
						pol->uv.Add(String::atoi(pobj));	// UV index.
						pobj = NextEntry(pobj, eobj, true);
					}
				}

				if	(pobj[0] == '/')
				{
					pobj++;
					pol->nrm.Add(String::atoi(pobj));		// Normal index.
					pobj = NextEntry(pobj, eobj, true);
				}
			}
		}
		else
			pobj = NextEntry(RunToEOL(pobj, eobj), eobj);
	}

	// Drop OBJ.
	obj.Free();

	// Transfer vertice.
	if	(geo->vtx.Allocate(vtx_list.GetCount()))
	{
		uint cvtx = 0;
		ListForeachPtr(Vector4 *, v, vtx_list)
			geo->vtx[cvtx++] = *v;
	}
	else
		__LOG_E__ << "Failed to allocate " << vtx_list.GetCount() << " vertices.\n";

	ListDeleteAllPtr(Vector4 *, vtx_list)

	// Transfer polygon.
	if	(geo->pol.Allocate(pol_list.GetCount()))
	{
		uint binding_count = 0;
		ListForeachPtr(ObjPoly *, p, pol_list)
			binding_count += p->vtx.GetCount();

		geo->binding.Allocate(binding_count);

		Array <Vector2> flat_uv;

		if	(has_uv)
			if	(flat_uv.Allocate(binding_count))
			{
				geo->uv[0].Allocate(binding_count);

				Vector2 *pflat_uv = &flat_uv[0];
				ListForeachPtr(Vector2 *, uv, uv_list)
					*pflat_uv++ = *uv;

				ListDeleteAllPtr(Vector2 *, uv_list)
			}

		Vector2 *puv = &geo->uv[0][0];

		if	(geo->binding)
		{
			uint *pbind = &geo->binding[0];

			uint cpol = 0;
			ListForeachPtr(ObjPoly *, p, pol_list)
			{
				Polygon *pol = &geo->pol[cpol];

				pol->vtx_count = (ushort)p->vtx.GetCount();
				pol->binding = pbind;
				pol->material = 0;

				for (int v = p->vtx.GetCount(); v > 0; --v)
					*pbind++ = p->vtx.ObjectAt(v - 1) - 1;

				ListForeachPtr(int, i, p->uv)
					*puv++ = flat_uv[i - 1];
			}
		}
		else
			__LOG_E__ << "Failed to allocate " << geo->binding.GetCount() << " polygon binding entries.\n";
	}
	else
		__LOG_E__ << "Failed to allocate " << geo->pol.GetCount() << " polygons.\n";

	ListDeleteAllPtr(ObjPoly *, pol_list)

	// Load materials.
	geo->material_table.Allocate(1);
	geo->material_table[0].name = "Default";

	// Setup defaults.
	geo->ComputeVertexNormal();

	return geo.Detach();
}
//------------------------------------------------------------------------------
