/* -*- mia-c++  -*-
 *
 * This file is part of MIA - a toolbox for medical image analysis 
 * Copyright (c) Leipzig, Madrid 1999-2017 Gert Wollny
 *
 * MIA 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 3 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 MIA; if not, see <http://www.gnu.org/licenses/>.
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <iostream>
#include <fstream>
#include <algorithm>
#include <sstream>
#include <cstring>

#include <mia/core/file.hh>
#include <mia/core/filter.hh>
#include <mia/core/msgstream.hh>
#include <mia/mesh/triangularMesh.hh>
#include <mia/mesh/triangulate.hh>

NS_BEGIN(ply_mesh_io)

NS_MIA_USE;
using namespace std;

static char const * const format = "ply";

static const int buflen = 2048; 

class TPlyMeshIO: public CMeshIOPlugin {
public:
	TPlyMeshIO();

private:
	virtual PTriangleMesh do_load(string const &  filename)const;
	virtual bool do_save(string const &  filename, const CTriangleMesh& data)const;

	virtual const std::string  do_get_descr()const;
	bool do_save_it(const CTriangleMesh& mesh, std::ostream& os)const;
};

extern "C" EXPORT CPluginBase *get_plugin_interface()
{
	return new TPlyMeshIO();
}

TPlyMeshIO::TPlyMeshIO():CMeshIOPlugin(format)
{
	add_suffix(".ply");
	add_suffix(".PLY");

}

const string  TPlyMeshIO::do_get_descr()const
{
	return string("Ply triangle mesh input/output support");
}

// read the next line and skip comments
// 
static void get_line(char *buffer, int buflen, string const &  filename, FILE *file)
{
	do {
		if (!fgets(buffer, buflen-1, file)) {
			throw create_exception<runtime_error>("Ply: Bougus file '", filename, "'");		
		}
	} while (!strncmp(buffer, "comment ", 8));
	
	// ensure the buffer is null-terminated 
	buffer[buflen-1] = 0; 
	cvdebug() << "Read line '"  << buffer << "'\n";

}


static vector<pair<string, vector<string>> > get_properties(char *buffer, int buflen, string const &filename, FILE *file)
{
	vector<pair<string, vector<string>>> result;
	while (!strncmp(buffer, "property ", 9)) {

		// in get_line buffer is ensured to be null terminated 
		// coverity[TAINTED_SCALAR] 
		istringstream is(buffer);
		string key, name, type;  
		is >> key >> type;
		if (type == "list") {
			vector<string> s;
			while (is.eof()) {
				is >> name;
				if (!name.empty()) 
					s.push_back(name); 
			}
		}else{ 
			is >> name;
			result.push_back(make_pair(type, vector<string>{name}));
		}
		get_line(buffer, buflen, filename, file);
	}
	return result; 
}

map<string, int> key_flag_mapping = {
	{"x", 1},
	{"y", 2},
	{"z", 4},
	{"nx", 8},
	{"ny",16},
	{"nz",32},
	{"red", 64},
	{"green",128},
	{"blue",256},
	{"scale",512}}; 

void read_vertex_data(CTriangleMesh::CVertexfield& v,
		      CTriangleMesh::PNormalfield n, 
		      CTriangleMesh::PColorfield c,
		      CTriangleMesh::PScalefield s,
		      const string&  filename, FILE *file)
{
	char buffer[buflen +1]; 
	for (unsigned i = 0; i < v.size(); ++i) {
		get_line(buffer, buflen, filename, file);

		// coverity[TAINTED_SCALAR] 
		istringstream buf(buffer);

		buf >> v[i].x >> v[i].y >> v[i].z;
		if (n) {
			buf >> (*n)[i].x >> (*n)[i].y >> (*n)[i].z;
		}
		if (c) {
			buf >> (*c)[i].x >> (*c)[i].y >> (*c)[i].z;
		}
		if (s) {
			buf >> (*s)[i];
		}
	}
}

void read_faces(CTriangleMesh::CTrianglefield& triangles, unsigned n_faces, 
		const CTriangleMesh::CVertexfield& vertices,
		const string&  filename, FILE *file)
{
	char buffer[buflen]; 
	unsigned count;
	vector<unsigned> v(3);
	typedef TPolyTriangulator<CTriangleMesh::CVertexfield, vector<unsigned int> >  CPolyTriangulator;
	
	CPolyTriangulator triangulator(vertices); 
	
	for (unsigned i = 0; i < n_faces; ++i) {
		get_line(buffer, buflen, filename, file);
		
		// coverity[TAINTED_SCALAR] 
		istringstream buf(buffer);
		buf >> count;
		if (count > v.max_size()) {
			throw create_exception<runtime_error>("PLY: ", filename, ": ", count, " vertices specified ",
							      "but implementation only supports up to ", v.max_size()); 
		}
		
		v.resize(count);
		for (unsigned i = 0; i < count; ++i) {
			buf >> v[i];
		}
		if (count < 3) {
			cvwarn() << "PLY_Face with less than 3 vertices in '" << filename << "' ignoring";
		}
		if (count > 3) {
			vector<CTriangleMesh::triangle_type> tri; 
			triangulator.triangulate(tri, v);
			for (auto t: tri)
				triangles.push_back(t); 
		}else{
			cvdebug() << "Read triangle <" << v << "> from buffer '" << buffer << "'\n"; 
			triangles.push_back(CTriangleMesh::triangle_type(v[0], v[1], v[2])); 
		}
	}
}

pair<int, map<string, int>> get_data_flags(const vector<pair<string, vector<string>> >& vertex_properties,
					   const string& filename)
{
	int available_flags = 0;
	
	map<string, int> mapping; 
	for (unsigned i = 0; i < vertex_properties.size(); ++i) {
		mapping[vertex_properties[i].first] = i;

		auto key_flag = key_flag_mapping.find(vertex_properties[i].second[0]); 
		if (key_flag != key_flag_mapping.end()) {
			available_flags |= key_flag->second;
			cvdebug() << "available_flags= " << available_flags << " with key " << vertex_properties[i].second[0] << "\n"; 
		} else
			cvwarn() << "PLY: unsupported property '" << vertex_properties[i].second[0]
				 << "' found in '" << filename << ", ignoring\n";
	}

	int type_flags = 0; 
	if ((available_flags  & 7) == 7)
		type_flags |= CTriangleMesh::ed_vertex;
	
	if ((available_flags  & 0x38) == 0x38)
		type_flags |= CTriangleMesh::ed_normal; 

	if ((available_flags  & 0x1C0) == 0x1C0)
		type_flags |= CTriangleMesh::ed_color;

	if ((available_flags  & 0x200) == 0x200)
		type_flags |= CTriangleMesh::ed_scale;
	
	return make_pair(type_flags, mapping); 
	
}

PTriangleMesh TPlyMeshIO::do_load(string const &  filename) const
{
	cvdebug() << "Load as PLY?\n";
	char buffer[buflen];

	// make sure buffer is null-terminated 
	int n_vertices = 0;
	unsigned n_face = 0;

	CInputFile f(filename);
	if (!f)
		return PTriangleMesh();

	if ( (!fgets(buffer, buflen, f)) || strncmp(buffer,"ply",3)) {
		cvdebug() << "Not a PLY file\n"; 
		return PTriangleMesh();
	}

	if ( (!fgets(buffer, buflen, f)) || strncmp(buffer,"format ",7)) {
		throw create_exception<runtime_error>("Ply: Format specifier missing '", filename, "'");
	}

	// now we must check the type of file, only support ascii though
	
	get_line(buffer, buflen, filename, f);
	
	if (sscanf(buffer,"element vertex %d",&n_vertices)!= 1)
		throw create_exception<runtime_error>("Ply: Bougus file '", filename,
						      "', can't parse vertex count from '",buffer,"'.");
	
	get_line(buffer, buflen, filename, f);

	auto vertex_properties = get_properties(buffer, buflen, filename, f); 

	if (sscanf(buffer,"element face %d",&n_face)!= 1)
		throw create_exception<runtime_error>("Ply: Unsupported file '", filename, "', can't parse face count from '",
						      buffer, "'");

	get_line(buffer, buflen, filename, f);
	auto face_properties = get_properties(buffer, buflen, filename, f); 

	if (strncmp(buffer,"end_header",10))
		throw create_exception<runtime_error>("Ply: Header end marker not found in '", filename, "'.");


	CTriangleMesh::PVertexfield vertices(new CTriangleMesh::CVertexfield(n_vertices));
	CTriangleMesh::PNormalfield normals;
	CTriangleMesh::PColorfield colors;
	CTriangleMesh::PScalefield scales; 

	auto flags_and_map = get_data_flags(vertex_properties, filename);
	auto flags = flags_and_map.first; 
	
	if (!(flags & CTriangleMesh::ed_vertex))
		throw create_exception<runtime_error>("Ply: No supported vertex properties found in '", filename, "'.");

	if (flags & CTriangleMesh::ed_normal) {
		cvdebug() << "PLY: have normals\n"; 
		normals.reset(new CTriangleMesh::CNormalfield(n_vertices));
	}
	if (flags & CTriangleMesh::ed_color)
		colors.reset(new CTriangleMesh::CColorfield(n_vertices));
	if (flags & CTriangleMesh::ed_scale)
		scales.reset(new CTriangleMesh::CScalefield(n_vertices));

	read_vertex_data(*vertices, normals, colors, scales, filename, f);
	
	CTriangleMesh::PTrianglefield triangles(new  CTriangleMesh::CTrianglefield);

	if (n_face > triangles->max_size()) {
		throw create_exception<runtime_error>("PLY: ", filename, ": ", n_face, " triangles specified ",
						      "but implementation only supports up to ", triangles->max_size()); 
	}
		
	triangles->reserve(n_face); 

	read_faces(*triangles, n_face, *vertices, filename, f);

	return PTriangleMesh(new CTriangleMesh(triangles, vertices, normals, colors, scales));
}

template <class T>
class FWriteElement {
	ostream& os;
public:
	FWriteElement(ostream& __os):os(__os){};
	void operator()(const T3DVector<T>& elm) {
		os  <<  "3 " << elm.x << " " <<  elm.y << " " <<  elm.z << "\n";
	}
};

class CElementWriter {
public: 
	CElementWriter(): m_next(nullptr) {}
	void write(const CTriangleMesh& mesh, ostream& os, unsigned i)  {
		do_write(mesh, os, i);
		if (m_next) {
			os << " "; 
			m_next->write(mesh, os, i);
		}
	}

	void append(CElementWriter *w) {
		if (m_next)
			m_next->append(w);
		else
			m_next.reset(w); 
	}
private:
	virtual void do_write(const CTriangleMesh& mesh, ostream& os, unsigned i) = 0; 
	unique_ptr<CElementWriter> m_next; 
}; 

class CVertexWriter : public CElementWriter{
	virtual void do_write(const CTriangleMesh& mesh, ostream& os, unsigned i) {
		auto v = mesh.vertex_at(i);
		os << v.x << " " << v.y << " " << v.z; 
	}
}; 

class CNormalWriter : public CElementWriter{
	virtual void do_write(const CTriangleMesh& mesh, ostream& os, unsigned i) {
		auto v = mesh.normal_at(i);
		os << v.x << " " << v.y << " " << v.z; 
	}
}; 

class CColorWriter : public CElementWriter{
	virtual void do_write(const CTriangleMesh& mesh, ostream& os, unsigned i) {
		auto v = mesh.color_at(i);
		os << v.x << " " << v.y << " " << v.z; 
	}
}; 

class CScaleWriter : public CElementWriter{
	virtual void do_write(const CTriangleMesh& mesh, ostream& os, unsigned i) {
		auto v = mesh.scale_at(i);
		os << v; 
	}
}; 


bool TPlyMeshIO::do_save_it(const CTriangleMesh& mesh, ostream& os)const
{
	os << "ply\n";
	os << "format ascii 1.0\n";
	os << "element vertex " <<  mesh.vertices_size() << "\n";
	// add properties
	os << "property float32 x\n";
	os << "property float32 y\n";
	os << "property float32 z\n";

	CVertexWriter writer; 
	
	if (mesh.get_available_data() & CTriangleMesh::ed_normal) {
		os << "property float32 nx\n";
		os << "property float32 ny\n";
		os << "property float32 nz\n";
		writer.append(new CNormalWriter); 
	}
	
	if (mesh.get_available_data() & CTriangleMesh::ed_color) {
		os << "property float32 red\n";
		os << "property float32 green\n";
		os << "property float32 blue\n";
		writer.append(new CColorWriter); 
	}
	
	if (mesh.get_available_data() & CTriangleMesh::ed_scale) {
		os << "property float32 scale\n";
		writer.append(new CScaleWriter); 
	}
	os << "element face " << mesh.triangle_size() << "\n";
	os << "property list uint8 uint32 vertex_index\n"; 
	os << "end_header\n";

	for (unsigned i = 0; i < mesh.vertices_size(); ++i) {
		writer.write(mesh, os, i);
		os << "\n"; 
	}

	for_each(mesh.triangles_begin(),mesh.triangles_end(), FWriteElement<unsigned int>(os));
	
	return true;
}

bool TPlyMeshIO::do_save(string const &  filename, const CTriangleMesh& mesh) const
{
	if ( filename == "-")
		return do_save_it(mesh, cout);
	else {
		ofstream os(filename.c_str());
		return do_save_it(mesh, os);
	}

}

NS_END





