// 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
		\author Timothy M. Shead (tshead@k-3d.com)
*/

#include <k3dsdk/geometry.h>
#include <k3dsdk/object.h>
#include <k3dsdk/imaterial.h>
#include <k3dsdk/persistence.h>
#include <k3dsdk/material.h>
#include <k3dsdk/material_collection.h>
#include <k3dsdk/measurement.h>
#include <k3dsdk/mesh_source.h>
#include <k3dsdk/module.h>
#include <k3dsdk/path_data.h>
#include <k3dsdk/plugins.h>
#include <k3dsdk/transform.h>

#include <iterator>

#ifdef SDPWIN32
#define DEFAULT_FONT boost::filesystem::path("c:\\windows\\fonts\\arial.ttf", boost::filesystem::native)
#else
#define DEFAULT_FONT boost::filesystem::path("/usr/X11R6/lib/X11/fonts/truetype/arial.ttf", boost::filesystem::native)
#endif

#include <ft2build.h>
#include FT_FREETYPE_H

namespace libk3dfreetype2
{

namespace detail
{

/// Defines an arbitrary collection of control points
typedef std::vector<k3d::vector3> control_points_t;
/// Defines a contour (a closed loop defined by a set of control points)
typedef control_points_t contour_t;
/// Defines a collection of contours
typedef std::vector<contour_t> contours_t;

/// Bezier curve type (True Type fonts use conic bezier curves, Type 1 use cubic ones)
typedef enum
{
	LINEAR,
	CONIC,
	CUBIC

} point_t;

/// Returns the quadrant a point is in WRT the given origin (note: works in 2D by ignoring the Z coordinate!)
int quadrant(const k3d::vector3 Point, const k3d::vector3 Origin)
{
	return Point[0] < Origin[0] ? (Point[1] < Origin[1] ? 2 : 1) : (Point[1] < Origin[1] ? 3 : 0);
}


/// Point-in-poly test using winding angles (note: works in 2D by ignoring the Z coordinate)
bool point_in_contour(const k3d::vector3& Point, const contour_t& Contour)
{
	return_val_if_fail(Contour.size(), false);
	
	int winding = 0;
	k3d::vector3 last_point = Contour.back();
	int last_quadrant = quadrant(last_point, Point);

	for(contour_t::const_iterator point = Contour.begin(); point != Contour.end(); ++point)
		{	
			int current_quadrant = quadrant(*point, Point);
			
			if(current_quadrant != last_quadrant)
				{
					if(((last_quadrant+1)&3) == current_quadrant)
						{
							++winding;
						}
					else if(((current_quadrant+1)&3) == last_quadrant)
						{
							--winding;
						}
					else
						{
							double a = last_point[1] - (*point)[1];
							a *= (Point[0] - last_point[0]);
							double b = last_point[0] - (*point)[0];
							a += last_point[1] * b;
							b *= Point[1];

							if(a > b)
								winding += 2;
							else
								winding -= 2;
						}
						
					last_quadrant = current_quadrant;
				}
			last_point = *point;
		}
		
	return winding ? true : false;
}

/// Evaluate Bezier curve, order depends on the number of control points ...
const control_points_t evaluate_curve(const control_points_t& ControlPoints, const unsigned long CurveDivisions)
{
	control_points_t output;

	for(unsigned long i = 0; i != CurveDivisions; ++i)
		output.push_back(k3d::Bezier<k3d::vector3>(ControlPoints, static_cast<double>(i+1) / static_cast<double>(CurveDivisions+1)));

	return output;
}

class freetype_library
{
public:
	freetype_library() :
		m_initialized(0 == FT_Init_FreeType(&m_library))
	{
	}

	~freetype_library()
	{
		if(m_initialized)
			FT_Done_FreeType(m_library);
	}

	operator bool()
	{
		return m_initialized;
	}

	operator FT_Library()
	{
		return m_library;
	}

private:
	const bool m_initialized;
	FT_Library m_library;
};

class freetype_face
{
public:
	freetype_face(FT_Library Library, const boost::filesystem::path& Path) :
		m_initialized(0 == FT_New_Face(Library, Path.native_file_string().c_str(), 0, &m_face))
	{
	}

	~freetype_face()
	{
		if(m_initialized)
			FT_Done_Face(m_face);	
	}

	operator bool()
	{
		return m_initialized;
	}

	operator FT_Face()
	{
		return m_face;
	}

	FT_Face& face()
	{
		return m_face;
	}

private:
	const bool m_initialized;
	FT_Face m_face;
};

} // namespace detail

/////////////////////////////////////////////////////////////////////////////
// poly_text_implementation

class poly_text_implementation :
	public k3d::material_collection<k3d::mesh_source<k3d::persistent<k3d::object> > >
{
	typedef k3d::material_collection<k3d::mesh_source<k3d::persistent<k3d::object> > > base;

public:
	poly_text_implementation(k3d::idocument& Document) :
		base(Document),
		m_font_path(k3d::init_name("font") + k3d::init_description("Font path [string]") + k3d::init_value(DEFAULT_FONT) + k3d::init_document(Document)),
		m_text(k3d::init_name("text") + k3d::init_description("Text [string]") + k3d::init_value<std::string>("Text!") + k3d::init_document(Document)),
		m_curve_divisions(k3d::init_name("curve_divisions") + k3d::init_description("Curve divisions [integer]") + k3d::init_value(3) + k3d::init_constraint(k3d::constraint::minimum(1UL)) + k3d::init_document(Document) + k3d::init_precision(0) + k3d::init_step_increment(1) + k3d::init_units(typeid(k3d::measurement::scalar))),
		m_height(k3d::init_name("height") + k3d::init_description("Font height [number]") + k3d::init_value(10.0) + k3d::init_document(Document) + k3d::init_precision(2) + k3d::init_step_increment(0.01) + k3d::init_units(typeid(k3d::measurement::distance)))
	{
		enable_serialization(k3d::persistence::proxy(m_font_path));
		enable_serialization(k3d::persistence::proxy(m_text));
		enable_serialization(k3d::persistence::proxy(m_curve_divisions));
		enable_serialization(k3d::persistence::proxy(m_height));

		register_property(m_font_path);
		register_property(m_text);
		register_property(m_curve_divisions);
		register_property(m_height);

		m_material.changed_signal().connect(SigC::slot(*this, &poly_text_implementation::on_reset_geometry));
		
		m_font_path.changed_signal().connect(SigC::slot(*this, &poly_text_implementation::on_reset_geometry));
		m_text.changed_signal().connect(SigC::slot(*this, &poly_text_implementation::on_reset_geometry));
		m_curve_divisions.changed_signal().connect(SigC::slot(*this, &poly_text_implementation::on_reset_geometry));
		m_height.changed_signal().connect(SigC::slot(*this, &poly_text_implementation::on_reset_geometry));
	
		m_output_mesh.need_data_signal().connect(SigC::slot(*this, &poly_text_implementation::on_create_geometry));
	}

	void on_reset_geometry()
	{
		m_output_mesh.reset();
	}
	
	k3d::mesh* on_create_geometry()
	{
		std::auto_ptr<k3d::mesh> mesh(new k3d::mesh());
		
		mesh->polyhedra.push_back(new k3d::polyhedron());
		k3d::polyhedron& polyhedron = *mesh->polyhedra.back();
		polyhedron.material = m_material.interface();

		const boost::filesystem::path font_path = m_font_path.property_value();
		const std::string text = m_text.property_value();
		const unsigned long curve_divisions = m_curve_divisions.property_value();
		const double height = m_height.property_value();

		detail::freetype_library ft_library;
		if(!ft_library)
			{
				std::cerr << error << "Error initializing FreeType library" << std::endl;
				return 0;
			}

		detail::freetype_face ft_face(ft_library, font_path);
		if(!ft_face)
			{
				std::cerr << error << "Error opening font file: " << font_path.native_file_string() << std::endl;
				return 0;
			}

		if(!FT_IS_SCALABLE(ft_face.face()))
			{
				std::cerr << error << "Not a scalable font: " << font_path.native_file_string() << std::endl;
				return 0;
			}
			
		const double normalize_height = 1.0 / static_cast<double>(ft_face.face()->bbox.yMax - ft_face.face()->bbox.yMin);

		// For each character in the string ...
		k3d::vector3 offset;
		for(std::string::const_iterator c = text.begin(); c != text.end(); ++c)
			{
				if(0 != FT_Load_Glyph(ft_face, FT_Get_Char_Index(ft_face, static_cast<FT_ULong>(*c)), FT_LOAD_NO_SCALE | FT_LOAD_IGNORE_TRANSFORM))
					{
						std::cerr << error << "Error loading glyph for " << font_path.native_file_string() << "[" << *c << "]" << std::endl;
						continue;
					}

				FT_Outline ft_outline = ft_face.face()->glyph->outline;
				
				detail::contours_t contours;
				for(short ft_contour = 0; ft_contour != ft_outline.n_contours; ++ft_contour)
					{
						detail::contour_t contour;
		
						const short ft_contour_begin = ft_contour ? ft_outline.contours[ft_contour-1] + 1 : 0;
						const short ft_contour_end = ft_outline.contours[ft_contour] + 1;
						detail::point_t last_point_type = detail::LINEAR;

						for(short ft_contour_point = ft_contour_begin; ft_contour_point != ft_contour_end; ++ft_contour_point)
							{
								const k3d::vector3 point = offset + k3d::vector3(ft_outline.points[ft_contour_point].x, ft_outline.points[ft_contour_point].y, 0) * normalize_height * height;
								const detail::point_t point_type = (ft_outline.tags[ft_contour_point] & 1) ? detail::LINEAR : (ft_outline.tags[ft_contour_point] & 2) ? detail::CUBIC : detail::CONIC;
								
								contour.push_back(point);

								if(detail::CONIC == point_type && detail::CONIC == last_point_type)
									{
										const k3d::vector3 p1 = *(contour.end()-1);
										const k3d::vector3 p2 = *(contour.end()-2);
										const k3d::vector3 p3 = (p1 + p2) / 2;

										contour.insert(contour.end() - 1, p3);

										detail::control_points_t curve = detail::evaluate_curve(detail::control_points_t(contour.end() - 4, contour.end()-1), curve_divisions);

										contour.erase(contour.end() - 3, contour.end() - 2);
										contour.insert(contour.end() - 2, curve.begin(), curve.end());
									}
								else if(detail::LINEAR == point_type && detail::CONIC == last_point_type)
									{
										return_val_if_fail(contour.size() > 2, 0);
										detail::control_points_t curve = detail::evaluate_curve(detail::control_points_t(contour.end() - 3, contour.end()), curve_divisions);
										contour.erase(contour.end() - 2, contour.end() - 1);
										contour.insert(contour.end() - 1, curve.begin(), curve.end());
									}
								else if(detail::LINEAR == point_type && detail::CUBIC == last_point_type)
									{
										return_val_if_fail(contour.size() > 3, 0);
										detail::control_points_t curve = detail::evaluate_curve(detail::control_points_t(contour.end() - 4, contour.end()), curve_divisions);
										contour.erase(contour.end() - 3, contour.end() - 1);
										contour.insert(contour.end() - 1, curve.begin(), curve.end());
									}

								last_point_type = point_type;
							}

						if(detail::CONIC == last_point_type)
							{
								return_val_if_fail(contour.size() > 1, 0);
								detail::control_points_t control_points(contour.end() - 2, contour.end());
								control_points.push_back(contour.front());
								
								detail::control_points_t curve = detail::evaluate_curve(control_points, curve_divisions);
								contour.erase(contour.end() - 1, contour.end());
								contour.insert(contour.end(), curve.begin(), curve.end());
							}
						else if(detail::CUBIC == last_point_type)
							{
								return_val_if_fail(contour.size() > 2, 0);
								detail::control_points_t control_points(contour.end() - 3, contour.end());
								control_points.push_back(contour.front());

								detail::control_points_t curve = detail::evaluate_curve(control_points, curve_divisions);
								contour.erase(contour.end() - 2, contour.end());
								contour.insert(contour.end(), curve.begin(), curve.end());
							}

						contours.push_back(contour);													
					}

				while(!contours.empty())
					{
						// Try to figure-out which contours are holes (this implementation assumes that holes always come last)
						detail::contours_t holes;
						for(detail::contours_t::iterator contour = contours.begin() + 1; contour != contours.end(); )
							{
								if(detail::point_in_contour(contour->front(), contours.front()))
									{
										holes.push_back(*contour);
										contour = contours.erase(contour);
									}
								else
									{
										++contour;
									}
							}
						
						// Create the original poly ...
						k3d::mesh::points_t points;
						for(detail::control_points_t::const_iterator point = contours.front().begin(); point != contours.front().end(); ++point)
							points.push_back(new k3d::point(*point));
							
						k3d::polyhedron::edges_t edges;
						for(k3d::mesh::points_t::const_iterator point = points.begin(); point != points.end(); ++point)
							edges.push_back(new k3d::split_edge(*point));
						k3d::loop_edges(edges.begin(), edges.end());
						
						mesh->points.insert(mesh->points.end(), points.begin(), points.end());
						polyhedron.edges.insert(polyhedron.edges.end(), edges.begin(), edges.end());
						polyhedron.faces.push_back(new k3d::face(edges.front()));
						k3d::face& face = *polyhedron.faces.back();
						
						// Add holes ...
						for(detail::contours_t::const_iterator h = holes.begin(); h != holes.end(); ++h)
							{
								k3d::mesh::points_t points;
								for(detail::control_points_t::const_iterator point = h->begin(); point != h->end(); ++point)
									points.push_back(new k3d::point(*point));
							
								k3d::polyhedron::edges_t edges;
								for(k3d::mesh::points_t::const_iterator point = points.begin(); point != points.end(); ++point)
									edges.push_back(new k3d::split_edge(*point));
								k3d::loop_edges(edges.begin(), edges.end());
							
								mesh->points.insert(mesh->points.end(), points.begin(), points.end());
								polyhedron.edges.insert(polyhedron.edges.end(), edges.begin(), edges.end());
								face.holes.push_back(edges.front());
							}
						
						// Keep on trucking ...
						contours.erase(contours.begin());
					}

				// Handle character offset ...
				offset += k3d::vector3(ft_face.face()->glyph->metrics.horiAdvance, 0, 0) * normalize_height * height;
			}

		return_val_if_fail(is_valid(polyhedron), 0);
		
		return mesh.release();
	}
	
	k3d::iplugin_factory& factory()
	{
		return get_factory();
	}

	static k3d::iplugin_factory& get_factory()
	{
		static k3d::plugin_factory<k3d::document_plugin<poly_text_implementation>, k3d::interface_list<k3d::imesh_source > > factory(
			k3d::uuid(0x9acaeaf1, 0x1fe74387, 0xae71cbb3, 0x9b5e33fd),
			"PolyText",
			"Generates polygonal text using Freetype 2",
			"Objects",
			k3d::iplugin_factory::STABLE);

		return factory;
	}

private:
	k3d_data_property(boost::filesystem::path, k3d::immutable_name, k3d::change_signal, k3d::with_undo, k3d::local_storage, k3d::no_constraint) m_font_path;
	k3d_data_property(std::string, k3d::immutable_name, k3d::change_signal, k3d::with_undo, k3d::local_storage, k3d::no_constraint) m_text;
	k3d_measurement_property(unsigned long, k3d::immutable_name, k3d::change_signal, k3d::with_undo, k3d::local_storage, k3d::with_constraint) m_curve_divisions;
	k3d_measurement_property(double, k3d::immutable_name, k3d::change_signal, k3d::with_undo, k3d::local_storage, k3d::no_constraint) m_height;
};

} // namespace libk3dfreetype2

K3D_MODULE_START(k3d::uuid(0xd0691ef7, 0x0d6c41c0, 0xa607bea2, 0x09d386f5),  Registry)
	Registry.register_factory(libk3dfreetype2::poly_text_implementation::get_factory());
K3D_MODULE_END

