/*
   model/objfile.cc
   This file is part of the Osirion project and is distributed under
   the terms of the GNU General Public License version 2
*/

#include "sys/sys.h"
#include "model/objfile.h"

#include <sstream>
#include <cstring>

/* NOTE: Does NOT support reverse indexes. Cannot find an example
	of this in use, or a program that will export using them.
*/

namespace model
{

// max geometry bounds
const float MAX_BOUNDS = 16384.0f;

OBJFile::OBJFile(std::string const &name)
{
	objfile_name.append(name);
	objfile_name.append(".obj");
	objfile_ifs.open(objfile_name);

	obj_box.assign(MAX_BOUNDS, - MAX_BOUNDS);

	// a single fragmentgroup will contain all the model polygons
	obj_fragmentgroup = new FragmentGroup();
	obj_fragmentgroup->set_type(FragmentGroup::None);
	
	// reset counters
	obj_normalcount = 0;
	
	obj_collisionmodel = nullptr;
	obj_load_clip = false;
}

OBJFile::~OBJFile()
{
	for (VertexList::iterator it = obj_vertexlist.begin(); it != obj_vertexlist.end(); ++it) {
		delete(*it).second;
	}
	obj_vertexlist.clear();

	for (NormalList::iterator it = obj_normallist.begin(); it != obj_normallist.end(); ++it) {
		delete(*it).second;
	}
	obj_normallist.clear();
	
	for (UVList::iterator it = obj_uvlist.begin(); it != obj_uvlist.end(); ++it) {
		delete(*it).second;
	}
	obj_uvlist.clear();

	for (TriList::iterator it = obj_trilist.begin(); it != obj_trilist.end(); ++it) {
		delete(*it).second;
	}
	obj_trilist.clear();

	for (QuadList::iterator it = obj_quadlist.begin(); it != obj_quadlist.end(); ++it) {
		delete(*it).second;
	}
	obj_quadlist.clear();
	
	if (objfile_ifs.is_open())
		objfile_ifs.close();
}

math::Vector3f * OBJFile::find_vertex(size_t index)
{
	VertexList::iterator it = obj_vertexlist.find(index);
	if (it == obj_vertexlist.end())
	{
		return 0;
	}
	else
	{
		return (*it).second;
	}
}

math::Vector3f *OBJFile::find_normal(size_t index)
{
	NormalList::iterator it = obj_normallist.find(index);
	if (it == obj_normallist.end())
	{
		return 0;
	}
	else
	{
		return (*it).second;
	}
}
	
math::Vector2f *OBJFile::find_uv(size_t index)
{
	UVList::iterator it = obj_uvlist.find(index);
	if (it == obj_uvlist.end())
	{
		return 0;
	}
	else
	{
		return (*it).second;
	}
}
	
bool OBJFile::read()
{
	char data[1024];
	memset(data, 0, sizeof(data));
	
	math::Vector3f zero;
	
	size_t current_m = 0;
	size_t index_line = 0;
	
	Fragment *fragment = 0;
	Material *material = 0;
	
	while (objfile_ifs.getline(data, sizeof(data) - 1)) {
		std::istringstream line(data);
		
		index_line++;
		
		std::string word;
		line >> word;
		
		if( word.compare("usemtl") == 0) { /* material definition */
			
			std::string materialname;
			material = 0;
			
			if (line >> materialname) 
			{
				for(MaterialList::iterator it = obj_materiallist.begin(); it != obj_materiallist.end(); ++it) 
				{
					if( (*it).second->name().compare(materialname) == 0) {
						material = (*it).second;
						current_m = (*it).first;
						break;
					}
				}
				
				if (!material) {
					material = Material::load(materialname);
					obj_materiallist[obj_materiallist.size()] = material;
					current_m = obj_materiallist.size()-1;
				}
			} else {
				con_warn << objfile_name << " invalid material definition at line " << index_line << std::endl;
			}
		
		
		} else if (word.compare("v") == 0) { /* new vertex */
			float x, y, z;
			line >> x >> y >> z;
			
			math::Vector3f *v = new math::Vector3f(x, y, z);
			obj_vertexlist[obj_vertexlist.size()] = v;
			obj_box.expand(*v * SCALE);
		
		} else if (word.compare("vt") == 0) { /* vertex texture coordinate */
			float u, v;
			line >> u >> v;
			
			math::Vector2f *uv = new math::Vector2f(u, v);
			obj_uvlist[obj_uvlist.size()] = uv;
		
		} else if (word.compare("vn") == 0) { /* vertex normal */
			float x, y, z;
			line >> x >> y >> z;
			
			math::Vector3f *nm = new math::Vector3f(x, y, z);
			nm->normalize();
			obj_normallist[obj_normalcount] = nm;
			obj_normalcount++;
		
		} else if (word.compare("f") == 0) { /* face/polygon */
			size_t v[4];
			size_t uv[4];
			size_t vn[4];
			
			bool have_vn[4], have_uv[4];
			size_t nslash;
			
			for (size_t j = 0; j < 4; j++)
			{
				v[j] = 0;
				uv[j] = 0;
				vn[j] = 0;
				have_vn[j] = false;
				have_uv[j] = false;
			}

			std::string strbuf(line.str());
			strbuf.erase(0,2); //remove "f "

			std::stringstream tmp(strbuf);
			
			std::vector<std::string> points;
			int i=0;
			while(!tmp.eof()) {
				nslash = 0;
				
				have_vn[i] = 0;
				have_uv[i] = 0;
				
				std::string point;
				if (!(tmp >> point))  // bordercase for space at end of face definition
					continue;

				for(size_t c=0; c<point.length(); c++) {
					if(point[c] == '/') {
						nslash++;
						have_uv[i]=true;
						point[c] = ' ';
					}
					if(point[c] == ' ' && point[c+1] == '/') {
						have_uv[i]=false;
						have_vn[i]=true;
						nslash=2;
						point.erase(c+1,1);
					}
				}
				
				if(nslash==2 && have_vn[i] == false) {
					have_vn[i] = true;
					have_uv[i] = true;
				}
				points.push_back(point);
				
				std::stringstream tmp2(point);
				tmp2 >> v[i];
				if(have_uv[i]) tmp2 >> uv[i];
				if(have_vn[i]) tmp2 >> vn[i];
				i++;
			}

			if(points.size() != 4 && points.size() != 3) {
				con_warn << objfile_name << " face with " << points.size() << " vertices at line " << index_line << std::endl;
				points.clear();
				continue;
			}
			
			if(points.size() == 3) {
				math::Vector3f *v0 = find_vertex(v[0]-1);
				math::Vector3f *v1 = find_vertex(v[1]-1);
				math::Vector3f *v2 = find_vertex(v[2]-1);
				
				
				math::Vector3f *n0 = find_normal(vn[0]-1);
				math::Vector3f *n1 = find_normal(vn[1]-1);
				math::Vector3f *n2 = find_normal(vn[2]-1);
				
				math::Vector2f *t0 = find_uv(uv[0]-1);
				math::Vector2f *t1 = find_uv(uv[1]-1);
				math::Vector2f *t2 = find_uv(uv[2]-1);
				
				if(!v0 || !v1 || !v2) {
					con_warn << objfile_name << " invalid vertex called from line " << index_line << std::endl;
					continue;
				}

				Triangle *triangle = new Triangle(*v0, *v1, *v2);

				if(!n0)
				{
					n0 = new math::Vector3f(normal(triangle->v0(), triangle->v1(), triangle->v2()));
					obj_normallist[vn[0]-1] = n0;
				}
				if(!n1)
				{	
					n1 = new math::Vector3f(normal(triangle->v1(), triangle->v2(), triangle->v0()));
					obj_normallist[vn[1]-1] = n1;
				}
				if(!n2)
				{	
					n2 = new math::Vector3f(normal(triangle->v2(), triangle->v0(), triangle->v1()));
					obj_normallist[vn[2]-1] = n2;
				}

				triangle->n0().assign(n0->x(), n0->y(), n0->z());
				triangle->n1().assign(n1->x(), n1->y(), n1->z());
				triangle->n2().assign(n2->x(), n2->y(), n2->z());
				
				if(t0)	triangle->t0().assign(t0->x(), 1.0 - t0->y());
				if(t1)	triangle->t1().assign(t1->x(), 1.0 - t1->y());
				if(t2)	triangle->t2().assign(t2->x(), 1.0 - t2->y());
					
				std::pair<size_t, Triangle *> poly(current_m, triangle);
				obj_trilist.push_back(poly);
		
			} else if(points.size() == 4) {
				math::Vector3f *v0 = find_vertex(v[0]-1);
				math::Vector3f *v1 = find_vertex(v[1]-1);
				math::Vector3f *v2 = find_vertex(v[2]-1);
				math::Vector3f *v3 = find_vertex(v[3]-1);
				
				math::Vector3f *n0 = find_normal(vn[0]-1);
				math::Vector3f *n1 = find_normal(vn[1]-1);
				math::Vector3f *n2 = find_normal(vn[2]-1);
				math::Vector3f *n3 = find_normal(vn[3]-1);
				
				math::Vector2f *t0 = find_uv(uv[0]-1);
				math::Vector2f *t1 = find_uv(uv[1]-1);
				math::Vector2f *t2 = find_uv(uv[2]-1);
				math::Vector2f *t3 = find_uv(uv[3]-1);
								
				if(!v0 || !v1 || !v2 || !v3) {
					con_warn << objfile_name << " invalid vertex called from line " << index_line << std::endl;
					continue;
				}
				
				Quad *quad = new Quad(*v0, *v1, *v2, *v3, zero);
				
				if(!n0)
				{
					n0 = new math::Vector3f(normal(quad->v0(), quad->v1(), quad->v3()));
					obj_normallist[vn[0]-1] = n0;
				}
				if(!n1)
				{	
					n1 = new math::Vector3f(normal(quad->v1(), quad->v2(), quad->v0()));
					obj_normallist[vn[1]-1] = n1;
				}
				if(!n2)
				{	
					n2 = new math::Vector3f(normal(quad->v2(), quad->v3(), quad->v1()));
					obj_normallist[vn[2]-1] = n2;
				}
				if(!n3)
				{	
					n2 = new math::Vector3f(normal(quad->v3(), quad->v0(), quad->v2()));
					obj_normallist[vn[3]-1] = n3;
				}

				quad->n0().assign(n0->x(), n0->y(), n0->z());
				quad->n1().assign(n1->x(), n1->y(), n1->z());
				quad->n2().assign(n2->x(), n2->y(), n2->z());
				quad->n3().assign(n3->x(), n3->y(), n3->z());
				
				if(t0)	quad->t0().assign(t0->x(), 1.0 - t0->y());
				if(t1)	quad->t1().assign(t1->x(), 1.0 - t1->y());
				if(t2)	quad->t2().assign(t2->x(), 1.0 - t2->y());
				if(t3)	quad->t3().assign(t3->x(), 1.0 - t3->y());
				
				std::pair<size_t, Quad *> poly(current_m, quad);
				obj_quadlist.push_back(poly);
			}
		}
	}
	
	//Go through tri and quad lists and create fragments for each material
	size_t total_m = obj_materiallist.size();
	for(size_t i = 0; i < total_m; i++)
	{
		material = obj_materiallist[i];
		
		if (material->has_flag_clip())		// add mesh to the collision model if the material has the Clip flag set
		{
			CollisionMesh *collisionmesh = new CollisionMesh();
			
			// add triangles to the collision mesh
			for(TriList::iterator it = obj_trilist.begin(); it != obj_trilist.end(); ++it)
			{
				if((*it).first == i)
				{
					collisionmesh->add_triangle((*it).second->v0() * SCALE, (*it).second->v1() * SCALE, (*it).second->v2() * SCALE);
				}
			}
			
			for(QuadList::iterator it = obj_quadlist.begin(); it != obj_quadlist.end(); ++it)
			{
				if((*it).first == i)
				{
					collisionmesh->add_triangle((*it).second->v0() * SCALE, (*it).second->v1() * SCALE, (*it).second->v2() * SCALE);
					collisionmesh->add_triangle((*it).second->v2() * SCALE, (*it).second->v3() * SCALE, (*it).second->v0() * SCALE);
				}
			}
			
			obj_collisionmodel->add_mesh(collisionmesh);
		}		
		else if (!material->has_flag_ignore())	// do not load meshes if the material has the Ignore flag
		{
			
			fragment = new Fragment(Fragment::Triangles, material);
			
			for(TriList::iterator it = obj_trilist.begin(); it != obj_trilist.end(); ++it) {
				if((*it).first == i) {
					fragment->add_vertex(((*it).second->v0() * SCALE) , (*it).second->n0(), (*it).second->t0(), false);
					fragment->add_vertex(((*it).second->v1() * SCALE) , (*it).second->n1(), (*it).second->t1(), false);
					fragment->add_vertex(((*it).second->v2() * SCALE) , (*it).second->n2(), (*it).second->t2(), false);
				}
			}
			
			if( fragment->structural_size() + fragment->detail_size() > 0 )
				obj_fragmentgroup->add_fragment(fragment);
				
			fragment = new Fragment(Fragment::Quads, material);
			
			for(QuadList::iterator it = obj_quadlist.begin(); it != obj_quadlist.end(); ++it) {
				if((*it).first == i) {
					fragment->add_vertex(((*it).second->v0() * SCALE) , (*it).second->n0(), (*it).second->t0(), false);
					fragment->add_vertex(((*it).second->v1() * SCALE) , (*it).second->n1(), (*it).second->t1(), false);
					fragment->add_vertex(((*it).second->v2() * SCALE) , (*it).second->n2(), (*it).second->t2(), false);
					fragment->add_vertex(((*it).second->v3() * SCALE) , (*it).second->n3(), (*it).second->t3(), false);
				}
			}
			
			if( fragment->structural_size() + fragment->detail_size() > 0 )
				obj_fragmentgroup->add_fragment(fragment);
		}
	}
	
	return true;
}

Model *OBJFile::load(const std::string &name)
{
	
	OBJFile objfile(name);
	if (!objfile.is_open()) {
		return nullptr;
	}

	// create a new model
	Model *model = new Model(name);

	// load clip into collision meshes
	// if the model has been loaded before (e.g. on r_restart), the collision model won't be reloaded 
	objfile.obj_collisionmodel = CollisionModel::find(name);
	if (!objfile.obj_collisionmodel)
	{
		objfile.obj_load_clip = true;
		objfile.obj_collisionmodel = new CollisionModel(name);
		CollisionModel::add(objfile.obj_collisionmodel);
	} else {
		objfile.obj_load_clip = false;
	}
	model->set_collisionmodel(objfile.obj_collisionmodel);
	
	if (!objfile.read() || !objfile.fragmentgroup()->size())
	{
		con_warn << "  " << objfile.name() << " empty model" << std::endl;
		return model;
	}
	
	// center model around (0,0,0) and set the bounding box
	math::Vector3f obj_center((objfile.box().min() + objfile.box().max()) * 0.5f);	
	model->model_box.assign(
		objfile.box().min() - obj_center,
		objfile.box().max() - obj_center
	);	
	model->set_radius(model->box().max().length());
	model->set_origin(obj_center * -1.0f);

	objfile.fragmentgroup()->set_location(obj_center * -1.0f);
	
	for (FragmentGroup::Fragments::const_iterator fit = objfile.fragmentgroup()->fragments().begin(); fit != objfile.fragmentgroup()->fragments().end(); ++fit)
	{
		const Fragment *fragment = (*fit);
		
		if(fragment->type() == Fragment::Triangles)
			model->model_tris_count += (fragment->structural_size() + fragment->detail_size()) / 3;
		else
			model->model_quad_count += (fragment->structural_size() + fragment->detail_size()) / 4;
	}

	model->add_group(objfile.fragmentgroup());

	con_debug << "  " << objfile.name() << " " << objfile.vertexcount() << " vertices " << model->model_tris_count << " triangles " << model->model_quad_count << " quads" << std::endl;
	
	return model;
}

} // namespace model