// K-3D
// Copyright (c) 1995-2004, Timothy M. Shead
//
// Contact: tshead@k-3d.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public
// License as published by the Free Software Foundation; either
// version 2 of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public
// License along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

/** \file
		\brief Implements the OBJReader that imports Wavefront .obj files
		\author Tim Shead (tshead@k-3d.com)
		\author Wladyslaw Strugala (fizws@julia.univ.gda.pl)
		\author Romain Behar (romainbehar@yahoo.com)
*/

#include <k3dsdk/classes.h>
#include <k3dsdk/color.h>
#include <k3dsdk/file_helpers.h>
#include <k3dsdk/idag.h>
#include <k3dsdk/ideletable.h>
#include <k3dsdk/idocument.h>
#include <k3dsdk/idocument_plugin_factory.h>
#include <k3dsdk/ifile_format.h>
#include <k3dsdk/igeometry_read_format.h>
#include <k3dsdk/imaterial.h>
#include <k3dsdk/imaterial_collection.h>
#include <k3dsdk/iobject.h>
#include <k3dsdk/iobject_collection.h>
#include <k3dsdk/material.h>
#include <k3dsdk/mesh.h>
#include <k3dsdk/module.h>
#include <k3dsdk/plugins.h>
#include <k3dsdk/property.h>
#include <k3dsdk/result.h>
#include <k3dsdk/selection.h>
#include <k3dsdk/string_modifiers.h>
#include <k3dsdk/utility.h>
#include <k3dsdk/vectors.h>

#include "helpers.h"

#include <boost/filesystem/fstream.hpp>
#include <boost/filesystem/path.hpp>

#include <iostream>
#include <map>

namespace libk3dgeometry
{

unsigned long obj_v_index(const unsigned long HeapSize, const long Index)
{
	if(Index > 0)
		return Index - 1;

	return HeapSize + Index;
}

/*
k3d::vector3 obj_texture_vertex(const k3d::vector3Array& Vertices, const long Index)
{
	static k3d::vector3 error;
	return_val_if_fail(Index, error);

	return (Index > 0) ? Vertices[Index-1] : Vertices[Vertices.size() + Index];
}
*/

k3d::point* obj_vertex(const std::vector<k3d::point*>& Vertices, const long Index)
{
	return_val_if_fail(Index, 0);

	return Vertices[obj_v_index(Vertices.size(), Index)];
}

bool get_obj_line(boost::filesystem::ifstream& File, std::string& LineBuffer)
{
	do
		{
			if(File.eof())
				return false;

			k3d::getline(File, LineBuffer);
			LineBuffer = k3d::trim(LineBuffer);
			while(LineBuffer[LineBuffer.size() - 1] == '\\')
			{
				std::string temp_buffer(LineBuffer.begin(), LineBuffer.end() - 1);

				std::string newline;
				k3d::getline(File, newline);
				LineBuffer = k3d::trim(temp_buffer + newline);
			}
		}
	// Skip empty line and comments
	while(!LineBuffer.size() || ('#' == *LineBuffer.begin()));

	return true;
}

void get_obj_doubles(std::istringstream& Stream, std::vector<double>& List)
{
	while(true)
		{
			double value;
			Stream >> value;

			if(Stream.fail())
				return;

			List.push_back(value);
		}
}

/////////////////////////////////////////////////////////////////////////////
// obj_reader_implementation

class obj_reader_implementation :
	public k3d::ifile_format,
	public k3d::igeometry_read_format,
	public k3d::ideletable
{
public:
	obj_reader_implementation()
	{
		m_current_mesh = 0;
		m_current_polyhedron = 0;
		m_current_linear_curve_group = 0;
		m_current_nupatch = 0;
	}

	unsigned long priority()
	{
		return 0;
	}

	bool query_can_handle(const boost::filesystem::path& FilePath)
	{
		return "obj" == k3d::file_extension(FilePath);
	}

	bool pre_read(k3d::idocument& Document, const boost::filesystem::path& FilePath)
	{
		return true;
	}

	bool read_options(k3d::idocument& Document, const boost::filesystem::path& FilePath)
	{
		return true;
	}

	bool read_file(k3d::idocument& Document, const boost::filesystem::path& FilePath);

	bool file_loop(k3d::idocument& Document, boost::filesystem::ifstream& File);

	bool load_bspline(k3d::idocument& Document, const bool rational, boost::filesystem::ifstream& file);

	k3d::iplugin_factory& factory()
	{
		return get_factory();
	}

	static k3d::iplugin_factory& get_factory()
	{
		static k3d::plugin_factory<k3d::application_plugin<obj_reader_implementation>, k3d::interface_list<k3d::igeometry_read_format> > factory(
			k3d::uuid(0x45a20d5f, 0xd2f447a9, 0x9d772381, 0xac833c39),
			"OBJReader",
			"Wavefront ( .obj )",
			"");

		return factory;
	}

private:
	std::map<std::string, k3d::imaterial*> m_materials;
	bool LoadMTL(k3d::idocument& Document, std::string filename);
	boost::filesystem::path m_file_path;

	// Mesh variables and functions
	k3d::mesh* m_current_mesh;
	k3d::polyhedron* m_current_polyhedron;
	k3d::linear_curve_group* m_current_linear_curve_group;
	k3d::nupatch* m_current_nupatch;

	k3d::iobject* m_current_mesh_object;

	bool create_mesh(k3d::idocument& Document)
	{
		// Create document object ...
		k3d::iobject* instance;
		k3d::mesh* const mesh = detail::create_mesh(Document, "OBJ import", m_current_mesh_object, instance);
		return_val_if_fail(mesh, false);
		m_current_mesh = mesh;

		// Reset vertices
		geometric_points.clear();
		geometric_weights.clear();

		return true;
	}

	bool create_polyhedron(k3d::idocument& Document)
	{
		if(!m_current_mesh)
			return_val_if_fail(create_mesh(Document), false);

		k3d::polyhedron* polyhedron = new k3d::polyhedron();
		return_val_if_fail(polyhedron, false);

		polyhedron->material = default_material;

		m_current_mesh->polyhedra.push_back(polyhedron);

		m_current_polyhedron = polyhedron;

		return true;
	}

	bool create_linear_curve_group(k3d::idocument& Document)
	{
		if(!m_current_mesh)
			return_val_if_fail(create_mesh(Document), false);

		k3d::linear_curve_group* const curve_group = new k3d::linear_curve_group();
		return_val_if_fail(curve_group, false);

		m_current_mesh->linear_curve_groups.push_back(curve_group);

		m_current_linear_curve_group = curve_group;

		return true;
	}

	bool create_nupatch(k3d::idocument& Document)
	{
		if(!m_current_mesh)
			return_val_if_fail(create_mesh(Document), false);

		k3d::nupatch* const nupatch = new k3d::nupatch();
		return_val_if_fail(nupatch, false);

		nupatch->material = default_material;

		m_current_mesh->nupatches.push_back(nupatch);

		m_current_nupatch = nupatch;

		return true;
	}

	// Temp variables
	std::map<std::string, k3d::polyhedron*> group_polyhedra;
	std::map<std::string, std::string> group_materials;
	k3d::imaterial* default_material;

	std::vector<k3d::point*> geometric_points;
	std::vector<double> geometric_weights;
	k3d::vector3Array texture_vertices;
	k3d::vector3Array normal_vertices;
	k3d::vector3Array parameter_vertices;

	std::string current_group_name;

	k3d::vector3 maxXYZvalue;
	k3d::vector3 minXYZvalue;
};

bool obj_reader_implementation::read_file(k3d::idocument& Document, const boost::filesystem::path& FilePath)
{
	// Save path
	m_file_path = FilePath;

	// Try to open the input file ...
	boost::filesystem::ifstream file(FilePath);
	if(!file.good())
		{
			std::cerr << warning << __PRETTY_FUNCTION__ << ": error opening [" << FilePath.native_file_string() << "]" << std::endl;
			return false;
		}

	// Setup variables ...
	default_material = dynamic_cast<k3d::imaterial*>(k3d::default_material(Document));
	current_group_name = std::string("default");

	maxXYZvalue = k3d::vector3(0, 0, 0);
	minXYZvalue = k3d::vector3(0, 0, 0);

	// Parse the stream ...
	while(!file.eof())
		if(!file_loop(Document, file))
			return false;

	// Assign materials ...
	std::map<std::string, k3d::polyhedron*>::iterator polyhedron;
	for(polyhedron = group_polyhedra.begin(); polyhedron != group_polyhedra.end(); polyhedron++)
		{
			std::string group_name = polyhedron->first;
			k3d::polyhedron* p = polyhedron->second;

			std::string material_name = "";
			if(group_materials.find(group_name) != group_materials.end())
				{
					material_name = group_materials[group_name];
				}

			if(material_name.size())
				{
					p->material = m_materials[material_name];
				}
			else
				{
					p->material = default_material;
				}
		}

	return true;
}

bool obj_reader_implementation::file_loop(k3d::idocument& Document, boost::filesystem::ifstream& file)
{
	// Grab a line
	std::string linebuffer;
	if(!get_obj_line(file, linebuffer))
		return true;

	// Extract a record type ...
	std::istringstream stream(linebuffer);
	std::string recordtype;
	stream >> recordtype;

	// Load material library file(s) ...
	if(recordtype == "mtllib")
		while(true)
			{
				std::string mtl_file_name;
				stream >> mtl_file_name;

				if(stream.fail())
					break;

				if(mtl_file_name.size())
					LoadMTL(Document, mtl_file_name);
			}

	// Group definition or reference ...
	else if(recordtype == "g")
		{
			// Need handle groups to handle materials
			// if "group" is used as reference ...
			// Handle all groups in line ...
			while(!stream.eof())
			{
				stream >> current_group_name;
				if(!current_group_name.size())
					current_group_name = "default";

				// If group doesn't exist, create new one
				if(group_polyhedra.find(current_group_name) != group_polyhedra.end())
					{
						m_current_polyhedron = group_polyhedra[current_group_name];
					}
				else
				{
					// Create a new polyhedron with an associated material for this group
					return_val_if_fail(create_polyhedron(Document), false);
					m_current_polyhedron->material = default_material;

					group_polyhedra[current_group_name] = m_current_polyhedron;
				}
			}
		}

	// Smooth group definition ...
	//else if(recordtype == "s")
	//	{
	//	}

	// Geometric object ...
	else if(recordtype == "o")
		{
			std::string name;
			stream >> name;

			return_val_if_fail(create_mesh(Document), false);
			m_current_mesh_object->set_name(k3d::trim(name));
		}

	// Geometric vertices ...
	else if(recordtype == "v")
		{
			if(!m_current_mesh)
				return_val_if_fail(create_mesh(Document), false);

			// Handle coordinates ...
			k3d::vector3 coords;
			stream >> coords;
			k3d::point* const point = new k3d::point(coords);
			return_val_if_fail(point, false);

			// calculate min max dimensions to be used
			// for textures restoring if they are absent in
			// processed .obj file
			if(geometric_points.size())
				{
					maxXYZvalue = k3d::vectorMax(maxXYZvalue, coords);
					minXYZvalue = k3d::vectorMin(minXYZvalue, coords);
				}
			else
				{
					maxXYZvalue = coords;
					minXYZvalue = coords;
				}

			geometric_points.push_back(point);
			if(stream.eof())
				geometric_weights.push_back(1.0);
			else
				{
					double w;
					stream >> w;
					geometric_weights.push_back(w);
				}

			m_current_mesh->points.push_back(point);
		}

	// Texture vertices ...
	else if(recordtype == "vt")
		{
			k3d::vector3 coords;
			stream >> coords;
			texture_vertices.push_back(coords);
		}

	// Vertex normals ...
	else if(recordtype == "vn")
		{
			k3d::vector3 coords;
			stream >> coords;
			normal_vertices.push_back(coords);
		}

	// Parameter space vertices ...
	else if(recordtype == "vp")
		{
			k3d::vector3 coords;
			stream >> coords;
			parameter_vertices.push_back(coords);
		}

	// Points ...
	else if(recordtype == "p")
		{
			unsigned long geometric_index = 0;
			stream >> geometric_index;
			return_val_if_fail(geometric_index, false);
		}

	// Lines ...
	else if(recordtype == "l")
		{
			if(!m_current_linear_curve_group)
				return_val_if_fail(create_linear_curve_group(Document), false);

			k3d::linear_curve* const curve = new k3d::linear_curve();
			return_val_if_fail(curve, false);

			while(!stream.eof())
				{
					unsigned long geometric_index = 0;
					stream >> geometric_index;
					return_val_if_fail(geometric_index, false);

					k3d::point* const point = obj_vertex(geometric_points, geometric_index);
					return_val_if_fail(point, false);

					curve->control_points.push_back(point);

					while(stream.peek() == '/')
						{
							stream.get();

							unsigned long texture_index = 0;
							stream >> texture_index;
							return_val_if_fail(texture_index, false);

							//pathpoint->Location()->SetTextureXYZ(obj_texture_vertex(texture_vertices, texture_index));
							//pathpoint->Location()->SetImplicitTextureXYZ(obj_texture_vertex(texture_vertices, texture_index));

							break;
						}
				}

			m_current_linear_curve_group->curves.push_back(curve);
		}

	// Faces ...
	else if(recordtype == "f")
		{
			if(!m_current_polyhedron)
				return_val_if_fail(create_polyhedron(Document), false);

			// Create a face ...
			unsigned long edge_number = 0;
			k3d::split_edge* previous_edge = 0;
			k3d::face* face = 0;

			while(!stream.eof())
				{
					long geometric_index = 0;
					stream >> geometric_index;
					return_val_if_fail(geometric_index, false);

					k3d::point* const point = obj_vertex(geometric_points, geometric_index);
					return_val_if_fail(point, false);

					k3d::split_edge* edge = new k3d::split_edge(point);
					return_val_if_fail(edge, false);

					if(!face)
						{
							face = new k3d::face(edge);
							return_val_if_fail(face, false);
							m_current_polyhedron->faces.push_back(face);
						}
					else
						{
							previous_edge->face_clockwise = edge;
						}

					m_current_polyhedron->edges.push_back(edge);
					previous_edge = edge;
					edge_number++;

					unsigned long texture_index = 0;
					if(stream.peek() == '/')
						{
							stream.get();

							if(stream.peek() != '/')
							{
								stream >> texture_index;
								return_val_if_fail(texture_index, false);

								//pathpoint->Location()->SetTextureXYZ(obj_texture_vertex(texture_vertices, texture_index));
								//pathpoint->Location()->SetImplicitTextureXYZ(obj_texture_vertex(texture_vertices, texture_index));
							}
						}

					if(!texture_index)
					{
						// Restore texture coords using
						// geometric coords...
						k3d::vector3 restored_texture, atemp;

						//restored_texture = point->Location()->LocalXYZ();
						atemp = maxXYZvalue-minXYZvalue;
						atemp[0] = fabs(atemp[0]);
						atemp[1] = fabs(atemp[1]);
						atemp[2] = fabs(atemp[2]);

						// Swap x or y if z coord is greater
						if(atemp[2]>atemp[0])
						{
							atemp[0] = atemp[2];
							restored_texture[0] = restored_texture[2];
						}
						else if(atemp[2]>atemp[1])
						{
							atemp[1] = atemp[2];
							restored_texture[1] = restored_texture[2];
						}

						//atemp[2] = 1.0;
						if(atemp[0] == 0.0) atemp[0] = 1.0;
						if(atemp[1] == 0.0) atemp[1] = 1.0;

						restored_texture[0] = (restored_texture[0]-minXYZvalue[0])/atemp[0];
						restored_texture[1] = (restored_texture[1]-minXYZvalue[1])/atemp[1];
						restored_texture[2] = 0.0;
						//pathpoint->Location()->SetTextureXYZ(restored_texture);
						//pathpoint->Location()->SetImplicitTextureXYZ(restored_texture);
					}

					if(stream.peek() == '/')
						{
							stream.get();

							unsigned long normal_index = 0;
							stream >> normal_index;
							return_val_if_fail(normal_index, false);

							// We ignore normal vertices

							// to get coorect value of stream.eof() at endl
							stream.peek();
						}
				}

			// Close loop
			if(face)
				{
					previous_edge->face_clockwise = face->first_edge;
				}
		}

	// Free-form curve or surface
	else if(recordtype == "cstype")
		{
			std::string type;
			stream >> type;
			bool rational = false;
			if(type == "rat")
				{
					rational = true;
					stream >> type;
				}

			if(type == "bspline")
				{
					load_bspline(Document, rational, file);
				}
			else
				std::cerr << debug << "cstype not supported : " << type << std::endl;
		}

	else if(recordtype == "group")
		{
			std::string group_name;
			stream >> group_name;

			current_group_name = group_name;
		}

	else if(recordtype == "usemtl")
		{
			std::string material_name;
			stream >> material_name;

			k3d::trim(material_name);

			if(m_materials.find(material_name) == m_materials.end())
				group_materials[current_group_name] = "default";
			else
				group_materials[current_group_name] = material_name;
		}

	else
		{
			std::cerr << debug << "Ignored .OBJ record : " << recordtype << std::endl;
		}

	return true;
}

bool obj_reader_implementation::load_bspline(k3d::idocument& Document, const bool rational, boost::filesystem::ifstream& file)
{
	std::string freeform_type("");

	while(true)
		{
			std::string line;
			if(!get_obj_line(file, line))
				return false;

			unsigned long u_degree;
			unsigned long v_degree;

			std::istringstream stream(line);
			std::string name;
			stream >> name;
			if(name == "end")
				break;
			else if(name == "deg")
				{
					stream >> u_degree;
					stream >> v_degree;
				}
			else if(name == "surf")
				{
					freeform_type = "surf";

					return_val_if_fail(create_nupatch(Document), false);

					m_current_nupatch->u_order = u_degree + 1;
					m_current_nupatch->v_order = v_degree + 1;

					double s0, s1, t0, t1;
					stream >> s0 >> s1 >> t0 >> t1;

					int index;
					while(true)
						{
							stream >> index;
							if(stream.fail())
								break;

							k3d::point* const position = obj_vertex(geometric_points, index);
							assert_warning(position);
							m_current_nupatch->control_points.push_back(k3d::nupatch::control_point(position, geometric_weights[obj_v_index(geometric_weights.size(), index)]));
						}
				}
			else if(name == "parm")
				{
					if(freeform_type == "surf")
						{
							std::string type;
							stream >> type;
							if(type == "u")
								get_obj_doubles(stream, m_current_nupatch->u_knots);
							else if(type == "v")
								get_obj_doubles(stream, m_current_nupatch->v_knots);
							else
								std::cerr << debug << "OBJ reader: Unkown cstype parm type '" << type << "'" << std::endl;
						}
				}
			else
				std::cerr << debug << "OBJ reader: Unhandled free-form item '" << name << "'" << std::endl;
		}

	assert_warning(is_valid(*m_current_nupatch));

	return true;
}

bool obj_reader_implementation::LoadMTL(k3d::idocument& Document, std::string filename)
{
	// Append the same path as with parent .obj file
	//  .mtl file name may contain path, so extract only
	//  its pure file name before appending
	boost::filesystem::path currentfile(filename, boost::filesystem::native);
	boost::filesystem::path file(m_file_path.branch_path() / currentfile.leaf());

	// Open mtl file ...
	boost::filesystem::ifstream mtl_file(file);

	if(!mtl_file.good())
	{
		std::cerr << warning << __PRETTY_FUNCTION__ << ": error opening material file [" << file.native_file_string() << "]" << std::endl;
		return false;
	}

	// For tests
	std::cerr << debug << "Loading MTL file : " << filename << std::endl;

	// Colors
	k3d::color color;
	// Material name
	std::string mtlmaterialname = "";

	// Parse the mtl file ...
	while(!mtl_file.eof())
	{
		// Grab one line at a time ...
		std::string mtllinebuffer;
		k3d::getline(mtl_file, mtllinebuffer);

		// Skip empty lines ...
		if(!mtllinebuffer.size())
			continue;

		// Skip comments ...
		if('#' == *mtllinebuffer.begin())
			continue;

		// Extract a record type ...
		std::istringstream mtlstream(mtllinebuffer);

		std::string mtlrecordtype;
		mtlstream >> mtlrecordtype;

		// Skip empty lines ...
		if(!mtlrecordtype.size())
			continue;

		if(mtlrecordtype == "newmtl")
		{
			mtlstream >> mtlmaterialname;
			k3d::trim(mtlmaterialname);

			if(!mtlmaterialname.size())
			{
				mtlmaterialname = "default";
				std::cerr << debug << "empty material name in file " << file.native_file_string() << " - setting: " << mtlmaterialname << std::endl;
			}

			if(m_materials.find(mtlmaterialname) == m_materials.end())
			{
				k3d::iobject* const material_object = k3d::create_document_plugin(k3d::classes::RenderManMaterial(), Document, "OBJ Material");
				return_val_if_fail(material_object, false);

				k3d::imaterial* mtl_material = dynamic_cast<k3d::imaterial*>(material_object);
				return_val_if_fail(mtl_material, false);

				material_object->set_name(mtlmaterialname);
				m_materials[mtlmaterialname] = mtl_material;
			}
		}
		else if(mtlrecordtype == "Ka")
		{
			mtlstream >> color;
/*
			if(m_materials.find(mtlmaterialname) != m_materials.end())
				m_materials[mtlmaterialname]->SetAmbient(color);
*/
		}
		else if(mtlrecordtype == "Kd")
		{
			mtlstream >> color;

			if(m_materials.find(mtlmaterialname) != m_materials.end())
				assert_warning(k3d::set_property_value(*(m_materials[mtlmaterialname]), "color", color));
		}
		else if(mtlrecordtype == "Ks")
		{
			mtlstream >> color;
/*
			if(m_materials.find(mtlmaterialname) != m_materials.end())
				m_materials[mtlmaterialname]->SetSpecular(color);
*/
		}
		else if(mtlrecordtype == "Ns")
		{
			// Material shininess
			double mtlShininess;
			mtlstream >> mtlShininess;

			// Ns range is 0..1000
			if(mtlShininess > 1000 || mtlShininess < 0)
				mtlShininess = 1000;

			// Now to range 0..1
			mtlShininess /= 1000;

/*
			// Using SetSpecularSize to set luminosity
			if(m_materials.find(mtlmaterialname) != m_materials.end())
				m_materials[mtlmaterialname]->SetSpecularSize(mtlShininess);
*/
		}
		else if(mtlrecordtype == "illum")
		{
			// Material luminance
			double mtlLum;
			mtlstream >> mtlLum;

			std::cerr << "Unhandled illuminance " << mtlLum << std::endl;
		}
		else if(mtlrecordtype == "map_Kd")
		{
			// Material texture
			std::string mtlKdTextureFileName;
			mtlstream >> mtlKdTextureFileName;

			std::cerr << "Unhandled map_Kd " << mtlKdTextureFileName << std::endl;
			// - the same texture file can be used for many materials !
		}
		else
		{
			std::cerr << "Unknown .mtl material record: " << mtlrecordtype << " will be ignored" << std::endl;
		}
	}

	mtl_file.close();

	return true;
}

k3d::iplugin_factory& obj_reader_factory()
{
	return obj_reader_implementation::get_factory();
}

} // namespace libk3dgeometry


