// 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 Decimation tool using QSlim's quadric error metrics
		\author Romain Behar (romainbehar@yahoo.com)
*/

#include <k3dsdk/basic_math.h>
#include <k3dsdk/object.h>
#include <k3dsdk/persistence.h>
#include <k3dsdk/property.h>
#include <k3dsdk/measurement.h>
#include <k3dsdk/mesh_filter.h>
#include <k3dsdk/module.h>
#include <k3dsdk/plugins.h>
#include <k3dsdk/utility.h>

#include "MxQSlim.h"

namespace libk3dqslim
{

namespace detail
{

MxStdModel* triangulate_mesh(const k3d::mesh& Mesh)
{
	// Output polyhedra as triangles
	k3d::polyhedron::faces_t new_faces;
	k3d::polyhedron::edges_t new_edges;
	k3d::mesh::points_t new_points;

	for(k3d::mesh::polyhedra_t::const_iterator polyhedron = Mesh.polyhedra.begin(); polyhedron != Mesh.polyhedra.end(); ++polyhedron)
		k3d::triangulate((*polyhedron)->faces.begin(), (*polyhedron)->faces.end(), std::back_inserter(new_faces), std::back_inserter(new_edges), std::back_inserter(new_points));

	k3d::mesh::points_t all_points;
	all_points.insert(all_points.end(), Mesh.points.begin(), Mesh.points.end());
	all_points.insert(all_points.end(), new_points.begin(), new_points.end());

	// New Mx-mesh
	MxStdModel* model = new MxStdModel(100, 100);
	return_val_if_fail(model, 0);

	std::map<k3d::point*, unsigned long> point_map;
	for(k3d::mesh::points_t::const_iterator point = all_points.begin(); point != all_points.end(); ++point)
		{
			point_map.insert(std::make_pair(*point, point_map.size()));
			const k3d::vector3 position = (*point)->position;
			Vec3 pos(-position[0], position[1], position[2]);
			model->add_vertex(pos);
		}

	for(k3d::polyhedron::faces_t::const_iterator face = new_faces.begin(); face != new_faces.end(); ++face)
		{
			k3d::split_edge* const e0 = (*face)->first_edge;
			k3d::split_edge* const e1 = e0 ? e0->face_clockwise : 0;
			k3d::split_edge* const e2 = e1 ? e1->face_clockwise : 0;

			model->add_face(point_map[e2->vertex], point_map[e1->vertex], point_map[e0->vertex]);
		}

	std::for_each(new_faces.begin(), new_faces.end(), k3d::delete_object());
	std::for_each(new_edges.begin(), new_edges.end(), k3d::delete_object());
	std::for_each(new_points.begin(), new_points.end(), k3d::delete_object());

	return model;
}

}

/////////////////////////////////////////////////////////////////////////////
// quadric_decimation_implementation

class quadric_decimation_implementation :
	public k3d::mesh_filter<k3d::persistent<k3d::object> >
{
	typedef k3d::mesh_filter<k3d::persistent<k3d::object> > base;

public:
	quadric_decimation_implementation(k3d::idocument& Document) :
		base(Document),
		m_face_number(k3d::init_name("face_number") + k3d::init_description("Target face number [integer]") + k3d::init_value(100) + k3d::init_constraint(k3d::constraint::minimum(1UL)) + k3d::init_precision(0) + k3d::init_step_increment(1) + k3d::init_units(typeid(k3d::measurement::scalar)) + k3d::init_document(Document)),
		m_contraction_type(k3d::init_name("contraction_type") + k3d::init_description("Stop function [enumeration]") + k3d::init_value(EDGE) + k3d::init_enumeration(contraction_values()) + k3d::init_document(Document)),
		m_placement_policy(k3d::init_name("placement_policy") + k3d::init_description("Placement policy [enumeration]") + k3d::init_value(OPTIMAL) + k3d::init_enumeration(placement_values()) + k3d::init_document(Document)),
		m_quadric_weighting(k3d::init_name("quadric_weighting") + k3d::init_description("Quadric weighting policy [enumeration]") + k3d::init_value(AREA) + k3d::init_enumeration(quadric_weighting_values()) + k3d::init_document(Document)),
		m_boundary_weight(k3d::init_name("boundary_weight") + k3d::init_description("Use boundary preservation planes with given weight [number]") + k3d::init_value(1000.0) + k3d::init_precision(2) + k3d::init_step_increment(0.1) + k3d::init_units(typeid(k3d::measurement::scalar)) + k3d::init_document(Document)),
		m_compactness_ratio(k3d::init_name("compactness_ratio") + k3d::init_description("Compactness ratio [number]") + k3d::init_value(k3d::radians<double>(0.0)) + k3d::init_precision(2) + k3d::init_step_increment(0.01) + k3d::init_units(typeid(k3d::measurement::angle)) + k3d::init_document(Document)),
		m_meshing_penalty(k3d::init_name("meshing_penalty") + k3d::init_description("Penalty for bad meshes [number]") + k3d::init_value(1.0) + k3d::init_precision(2) + k3d::init_step_increment(0.01) + k3d::init_units(typeid(k3d::measurement::scalar)) + k3d::init_document(Document))
	{
		enable_serialization(k3d::persistence::proxy(m_face_number));
		enable_serialization(k3d::persistence::proxy(m_contraction_type));
		enable_serialization(k3d::persistence::proxy(m_placement_policy));
		enable_serialization(k3d::persistence::proxy(m_quadric_weighting));
		enable_serialization(k3d::persistence::proxy(m_boundary_weight));
		enable_serialization(k3d::persistence::proxy(m_compactness_ratio));
		enable_serialization(k3d::persistence::proxy(m_meshing_penalty));

		register_property(m_face_number);
		register_property(m_contraction_type);
		register_property(m_placement_policy);
		register_property(m_quadric_weighting);
		register_property(m_boundary_weight);
		register_property(m_compactness_ratio);
		register_property(m_meshing_penalty);

		m_input_mesh.changed_signal().connect(SigC::slot(*this, &quadric_decimation_implementation::on_reset_geometry));
		m_face_number.changed_signal().connect(SigC::slot(*this, &quadric_decimation_implementation::on_reset_geometry));
		m_contraction_type.changed_signal().connect(SigC::slot(*this, &quadric_decimation_implementation::on_reset_geometry));
		m_placement_policy.changed_signal().connect(SigC::slot(*this, &quadric_decimation_implementation::on_reset_geometry));
		m_quadric_weighting.changed_signal().connect(SigC::slot(*this, &quadric_decimation_implementation::on_reset_geometry));
		m_boundary_weight.changed_signal().connect(SigC::slot(*this, &quadric_decimation_implementation::on_reset_geometry));
		m_compactness_ratio.changed_signal().connect(SigC::slot(*this, &quadric_decimation_implementation::on_reset_geometry));
		m_meshing_penalty.changed_signal().connect(SigC::slot(*this, &quadric_decimation_implementation::on_reset_geometry));
		m_output_mesh.need_data_signal().connect(SigC::slot(*this, &quadric_decimation_implementation::on_create_geometry));

		new_input = true;
	}

	void on_new_input()
	{
		new_input = true;
		on_reset_geometry();
	}

	void on_reset_geometry()
	{
		m_output_mesh.reset();
	}

	k3d::mesh* on_create_geometry()
	{
		// Get the input geometry ...
		k3d::mesh* const input = m_input_mesh.property_value();
		if(!input)
			return 0;

		// Load mesh into MxStdModel
		MxStdModel* m = detail::triangulate_mesh(*input);
		return_val_if_fail(m, 0);

		// Set current face number in case of new input
		if(new_input)
			{
				const unsigned long face_number = m->face_count();
				k3d::set_property_value(m_face_number, face_number);
				new_input = false;
			}

		// Init
		MxQSlim* slim = 0;
		MxEdgeQSlim* eslim = 0;
		MxFaceQSlim* fslim = 0;
		std::vector<MxEdge> target_edges;

		const bool will_use_fslim = m_contraction_type.property_value() == FACE;
		if(!slim)
			{
				if(will_use_fslim)
					slim = fslim = new MxFaceQSlim(*m);
				else
					slim = eslim = new MxEdgeQSlim(*m);
			}
		else
			{
				if(will_use_fslim)
					fslim = (MxFaceQSlim*)slim;
				else
					eslim = (MxEdgeQSlim*)slim;
			}

		switch(m_placement_policy.property_value())
			{
				case LINE: slim->placement_policy = 2; break;
				case ENDORMID: slim->placement_policy = 1; break;
				case ENDPOINTS: slim->placement_policy = 0; break;
				default: slim->placement_policy = 3;
			}
		slim->boundary_weight = m_boundary_weight.property_value();
		switch(m_quadric_weighting.property_value())
			{
				case UNIFORM: slim->weighting_policy = 0; break;
				case ANGLE: slim->weighting_policy = 2; break;
				default: slim->weighting_policy = 1;
			}
		slim->compactness_ratio = m_compactness_ratio.property_value();
		slim->meshing_penalty = m_meshing_penalty.property_value();
		slim->will_join_only = false;

		if(eslim && target_edges.size())
			eslim->initialize(target_edges, target_edges.size());
		else
			slim->initialize();

		// Decimation
		slim->decimate(m_face_number.property_value());

		// Output cleanup ...

		// First, mark stray vertices for removal
		for(unsigned long i = 0; i < m->vert_count(); ++i)
			if(m->vertex_is_valid(i) && m->neighbors(i).size() == 0)
				m->vertex_mark_invalid(i);

		// Compact vertex array so only valid vertices remain
		m->compact_vertices();

		// Create output geometry ...
		k3d::mesh* const output = new k3d::mesh();

		std::map<unsigned long, k3d::point*> point_map;
		for(unsigned int v = 0; v < m->vert_count(); v++)
			{
				k3d::vector3 position(m->vertex(v));
				position[0] = -position[0];

				k3d::point* point = new k3d::point(position);
				output->points.push_back(point);

				point_map[v] = point;
			}

		k3d::polyhedron* polyhedron = new k3d::polyhedron();
		return_val_if_fail(polyhedron, 0);
		output->polyhedra.push_back(polyhedron);
		for(unsigned int f = 0; f < m->face_count(); f++)
			if(m->face_is_valid(f))
				{
					unsigned long i1 = m->face(f).v[0];
					unsigned long i2 = m->face(f).v[1];
					unsigned long i3 = m->face(f).v[2];

					// Create a triangle ...
					k3d::split_edge* edge1 = new k3d::split_edge(point_map[i1]);
					k3d::split_edge* edge2 = new k3d::split_edge(point_map[i2]);
					k3d::split_edge* edge3 = new k3d::split_edge(point_map[i3]);

					// Invert face (because of coordinate system switch)
					edge1->face_clockwise = edge3;
					edge3->face_clockwise = edge2;
					edge2->face_clockwise = edge1;

					polyhedron->edges.push_back(edge1);
					polyhedron->edges.push_back(edge2);
					polyhedron->edges.push_back(edge3);

					k3d::face* const face = new k3d::face(edge1);
					return_val_if_fail(face, 0);

					polyhedron->faces.push_back(face);
				}

		// Copy materials ...
		if(input->polyhedra.size() && output->polyhedra.size())
			output->polyhedra.front()->material = input->polyhedra.front()->material;

		return output;
	}

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

	static k3d::iplugin_factory& get_factory()
	{
		static k3d::plugin_factory<
			k3d::document_plugin<quadric_decimation_implementation>,
				k3d::interface_list<k3d::imesh_source,
				k3d::interface_list<k3d::imesh_sink > > > factory(
				k3d::uuid(0x9bfe8fce, 0x3c7c4b00, 0x9e050d1e, 0x062059fa),
				"QuadricDecimation",
				"Surface simplification using quadric error metrics",
				"Objects",
				k3d::iplugin_factory::STABLE);

		return factory;
	}

private:
	/// Stores whether current input mesh has changed
	bool new_input;

	/// Enumerates contraction functions
	typedef enum
	{
		EDGE,
		FACE
	} contraction_t;

	friend std::ostream& operator << (std::ostream& Stream, const contraction_t& Value)
	{
		switch(Value)
			{
				case EDGE:
					Stream << "edge";
				break;
				case FACE:
					Stream << "face";
				break;
			}

		return Stream;
	}

	friend std::istream& operator >> (std::istream& Stream, contraction_t& Value)
	{
		std::string text;
		Stream >> text;

		if(text == "edge")
			Value = EDGE;
		else if(text == "face")
			Value = FACE;
		else
			std::cerr << __PRETTY_FUNCTION__ << ": unknown enumeration [" << text << "]" << std::endl;

		return Stream;
	}

	const k3d::ienumeration_property::values_t& contraction_values()
	{
		static k3d::ienumeration_property::values_t values;
		if(values.empty())
			{
				values.push_back(k3d::ienumeration_property::value_t("Edge", "edge", "Use edge contraction"));
				values.push_back(k3d::ienumeration_property::value_t("Face", "face", "Use face contraction"));
			}

		return values;
	}

	/// Enumerates placement policies
	typedef enum
	{
		OPTIMAL,
		LINE,
		ENDORMID,
		ENDPOINTS
	} placement_policy_t;

	friend std::ostream& operator << (std::ostream& Stream, const placement_policy_t& Value)
	{
		switch(Value)
			{
				case OPTIMAL:
					Stream << "optimal";
					break;
				case LINE:
					Stream << "line";
					break;
				case ENDORMID:
					Stream << "endormid";
					break;
				case ENDPOINTS:
					Stream << "endpoints";
					break;
			}

		return Stream;
	}

	friend std::istream& operator >> (std::istream& Stream, placement_policy_t& Value)
	{
		std::string text;
		Stream >> text;

		if(text == "optimal")
			Value = OPTIMAL;
		else if(text == "line")
			Value = LINE;
		else if(text == "endormid")
			Value = ENDORMID;
		else if(text == "endpoints")
			Value = ENDPOINTS;
		else
			std::cerr << __PRETTY_FUNCTION__ << ": unknown enumeration [" << text << "]" << std::endl;

		return Stream;
	}

	const k3d::ienumeration_property::values_t& placement_values()
	{
		static k3d::ienumeration_property::values_t values;
		if(values.empty())
			{
				values.push_back(k3d::ienumeration_property::value_t("Optimal", "optimal", "Use optimal placement policy"));
				values.push_back(k3d::ienumeration_property::value_t("Line", "line", "Use line placement policy"));
				values.push_back(k3d::ienumeration_property::value_t("Endormid", "endormid", "Use end-point or mid-point placement policy"));
				values.push_back(k3d::ienumeration_property::value_t("Endpoints", "endpoints", "Use end-points placement policy"));
			}

		return values;
	}

	/// Enumerates quadric weighting policies
	typedef enum
	{
		UNIFORM,
		AREA,
		ANGLE
	} quadric_weighting_t;

	friend std::ostream& operator << (std::ostream& Stream, const quadric_weighting_t& Value)
	{
		switch(Value)
			{
				case UNIFORM:
					Stream << "uniform";
					break;
				case AREA:
					Stream << "area";
					break;
				case ANGLE:
					Stream << "angle";
					break;
			}

		return Stream;
	}

	friend std::istream& operator >> (std::istream& Stream, quadric_weighting_t& Value)
	{
		std::string text;
		Stream >> text;

		if(text == "uniform")
			Value = UNIFORM;
		else if(text == "area")
			Value = AREA;
		else if(text == "angle")
			Value = ANGLE;
		else
			std::cerr << __PRETTY_FUNCTION__ << ": unknown enumeration [" << text << "]" << std::endl;

		return Stream;
	}

	const k3d::ienumeration_property::values_t& quadric_weighting_values()
	{
		static k3d::ienumeration_property::values_t values;
		if(values.empty())
			{
				values.push_back(k3d::ienumeration_property::value_t("Uniform", "uniform", "Use uniform quadric weighting"));
				values.push_back(k3d::ienumeration_property::value_t("Area", "area", "Use area quadric weighting"));
				values.push_back(k3d::ienumeration_property::value_t("Angle", "angle", "Use angle quadric weighting"));
			}

		return values;
	}

	k3d_measurement_property(unsigned long, k3d::immutable_name, k3d::change_signal, k3d::with_undo, k3d::local_storage, k3d::with_constraint) m_face_number;
	k3d_enumeration_property(contraction_t, k3d::immutable_name, k3d::change_signal, k3d::with_undo, k3d::local_storage, k3d::no_constraint) m_contraction_type;
	k3d_enumeration_property(placement_policy_t, k3d::immutable_name, k3d::change_signal, k3d::with_undo, k3d::local_storage, k3d::no_constraint) m_placement_policy;
	k3d_enumeration_property(quadric_weighting_t, k3d::immutable_name, k3d::change_signal, k3d::with_undo, k3d::local_storage, k3d::no_constraint) m_quadric_weighting;
	k3d_measurement_property(double, k3d::immutable_name, k3d::change_signal, k3d::with_undo, k3d::local_storage, k3d::no_constraint) m_boundary_weight;
	k3d_measurement_property(double, k3d::immutable_name, k3d::change_signal, k3d::with_undo, k3d::local_storage, k3d::no_constraint) m_compactness_ratio;
	k3d_measurement_property(double, k3d::immutable_name, k3d::change_signal, k3d::with_undo, k3d::local_storage, k3d::no_constraint) m_meshing_penalty;
};

/////////////////////////////////////////////////////////////////////////////
// quadric_decimation_factory

k3d::iplugin_factory& quadric_decimation_factory()
{
	return quadric_decimation_implementation::get_factory();
}

} // namespace libk3dqslim

K3D_MODULE_START(k3d::uuid(0xebf890d3, 0xfd344318, 0xa084edc5, 0x8cdc5761), Registry)
	Registry.register_factory(libk3dqslim::quadric_decimation_implementation::get_factory());
K3D_MODULE_END


