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

#include <k3dsdk/imaterial.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/noise.h>
#include <k3dsdk/object.h>
#include <k3dsdk/persistence.h>
#include <k3dsdk/plugins.h>
#include <k3dsdk/transform.h>

namespace libk3dmesh
{

/// A fractal terrain algorithm that creates heterogeneous terrains
/// based on fBM (from Perlin, "Texturing and Modeling, A Procedural Approach")

double get_elevation(const unsigned long XIndex, const unsigned long YIndex, const unsigned long Size, const std::vector<double>& ExponentArray, const double Frequency, const double NoiseOffset, const double Offset, const double Lacunarity, const double Octaves)
{
	// Sanity checks ...
	assert_warning(XIndex < Size);
	assert_warning(YIndex < Size);

	// Convert grid coordinates to the range [0, 1], [0, 1] ...
	double x = double(XIndex) / double(Size-1);
	double y = double(YIndex) / double(Size-1);

	// First, unscaled octave ...
	k3d::vector3 point(x * Frequency, y * Frequency, NoiseOffset);
	double value = Offset + k3d::noise(point);
	point *= Lacunarity;

	// Spectral construction inner loop ...
	for(int i = 1; i < Octaves; i++)
		{
			// Obtain displaced noise value ...
			double increment = Offset + k3d::noise(point);

			// Scale amplitude for this frequency ...
			increment *= ExponentArray[i];

			// Scale increment by current "altitude" ....
			increment *= value;

			// Add increment to value ...
			value += increment;

			// Raise spatial frequency ...
			point *= Lacunarity;
		}

	// Handle remainder in octaves ...
	double remainder = Octaves - int(Octaves);
	if(remainder)
		{
			double increment = Offset + k3d::noise(point) * ExponentArray.back();
			value += remainder * increment * value;
		}

	return value;
}

bool create_hfbm_triangle(k3d::polyhedron& Polyhedron, k3d::point* Point1, k3d::point* Point2, k3d::point* Point3)
{
	assert_warning(Point1);
	assert_warning(Point2);
	assert_warning(Point3);

	k3d::split_edge* edge1 = new k3d::split_edge(Point1);
	k3d::split_edge* edge2 = new k3d::split_edge(Point2);
	k3d::split_edge* edge3 = new k3d::split_edge(Point3);

	Polyhedron.edges.push_back(edge1);
	Polyhedron.edges.push_back(edge2);
	Polyhedron.edges.push_back(edge3);

	edge1->face_clockwise = edge3;
	edge3->face_clockwise = edge2;
	edge2->face_clockwise = edge1;

	k3d::face* const face = new k3d::face(edge1);
	return_val_if_fail(face, false);
	Polyhedron.faces.push_back(face);

	return true;
}

/////////////////////////////////////////////////////////////////////////////
// poly_terrain_hfbm_implementation

class poly_terrain_hfbm_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_terrain_hfbm_implementation(k3d::idocument& Document) :
		base(Document),
		m_iterations(k3d::init_name("iterations") + k3d::init_description("Iterations [integer]") + k3d::init_value(4) + k3d::init_document(Document) + k3d::init_constraint(k3d::constraint::minimum(1UL)) + k3d::init_precision(0) + k3d::init_step_increment(1) + k3d::init_units(typeid(k3d::measurement::scalar))),
		m_fractal_dimension(k3d::init_name("dimension") + k3d::init_description("Fractal dimension [number]") + k3d::init_document(Document) + k3d::init_value(0.5) + k3d::init_precision(2) + k3d::init_step_increment(0.1) + k3d::init_units(typeid(k3d::measurement::distance))),
		m_lacunarity(k3d::init_name("lacunarity") + k3d::init_description("Lacunarity [number]") + k3d::init_value(0.2) + k3d::init_precision(2) + k3d::init_step_increment(0.1) + k3d::init_units(typeid(k3d::measurement::distance)) + k3d::init_document(Document)),
		m_octaves(k3d::init_name("octaves") + k3d::init_description("Octaves [number]") + k3d::init_value(4.0) + k3d::init_precision(2) + k3d::init_step_increment(0.1) + k3d::init_units(typeid(k3d::measurement::distance)) + k3d::init_document(Document)),
		m_offset(k3d::init_name("offset") + k3d::init_description("Offset [number]") + k3d::init_value(0.0) + k3d::init_precision(2) + k3d::init_step_increment(0.1) + k3d::init_units(typeid(k3d::measurement::distance)) + k3d::init_document(Document)),
		m_frequency(k3d::init_name("frequency") + k3d::init_description("Frequency [number]") + k3d::init_value(6.0) + k3d::init_precision(2) + k3d::init_step_increment(0.1) + k3d::init_units(typeid(k3d::measurement::distance)) + k3d::init_document(Document)),
		m_noise_offset(k3d::init_name("noiseoffset") + k3d::init_description("Noise Offset [number]") + k3d::init_value(0.5) + k3d::init_precision(2) + k3d::init_step_increment(0.1) + k3d::init_units(typeid(k3d::measurement::distance)) + k3d::init_document(Document))
	{
		enable_serialization(k3d::persistence::proxy(m_iterations));
		enable_serialization(k3d::persistence::proxy(m_fractal_dimension));
		enable_serialization(k3d::persistence::proxy(m_lacunarity));
		enable_serialization(k3d::persistence::proxy(m_octaves));
		enable_serialization(k3d::persistence::proxy(m_offset));
		enable_serialization(k3d::persistence::proxy(m_frequency));
		enable_serialization(k3d::persistence::proxy(m_noise_offset));

		register_property(m_iterations);
		register_property(m_fractal_dimension);
		register_property(m_lacunarity);
		register_property(m_octaves);
		register_property(m_offset);
		register_property(m_frequency);
		register_property(m_noise_offset);

		m_material.changed_signal().connect(SigC::slot(*this, &poly_terrain_hfbm_implementation::on_reset_geometry));

		m_iterations.changed_signal().connect(SigC::slot(*this, &poly_terrain_hfbm_implementation::on_reset_geometry));
		m_fractal_dimension.changed_signal().connect(SigC::slot(*this, &poly_terrain_hfbm_implementation::on_reset_geometry));
		m_lacunarity.changed_signal().connect(SigC::slot(*this, &poly_terrain_hfbm_implementation::on_reset_geometry));
		m_octaves.changed_signal().connect(SigC::slot(*this, &poly_terrain_hfbm_implementation::on_reset_geometry));
		m_offset.changed_signal().connect(SigC::slot(*this, &poly_terrain_hfbm_implementation::on_reset_geometry));
		m_frequency.changed_signal().connect(SigC::slot(*this, &poly_terrain_hfbm_implementation::on_reset_geometry));
		m_noise_offset.changed_signal().connect(SigC::slot(*this, &poly_terrain_hfbm_implementation::on_reset_geometry));

		m_output_mesh.need_data_signal().connect(SigC::slot(*this, &poly_terrain_hfbm_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();

		// Calculate standard terrain parameters ...
		const unsigned long iterations = m_iterations.property_value();
		const unsigned long points = static_cast<unsigned long>(pow(2, iterations));
		const unsigned long segments = points - 1;

		const double terrain_width = 20.0;

		// Create points ...
		const double terrain_step = terrain_width / static_cast<double>(segments);
		double terrain_z = -terrain_width / 2;
		for(unsigned long z = 0; z < points; z++)
			{
				double terrain_x = -terrain_width / 2;
				for(unsigned long x = 0; x < points; x++)
					{
						mesh->points.push_back(new k3d::point(terrain_x, 0, terrain_z));
						terrain_x += terrain_step;
					}

				terrain_z += terrain_step;
			}

		// Create triangle grid ...
		for(unsigned long z = 0; z < segments; z++)
			for(unsigned long x = 0; x < segments; x++)
				{
					create_hfbm_triangle(polyhedron, mesh->points[z*points + x], mesh->points[z*points + x+1], mesh->points[(z+1)*points + x+1]);
					create_hfbm_triangle(polyhedron, mesh->points[z*points + x], mesh->points[(z+1)*points + x+1], mesh->points[(z+1)*points + x]);
				}

		// Calculate surface elevations ...
		std::vector<double> exponent_array;

		double frequency = 1.0;
		for(unsigned long i = 0; i <= m_octaves.value(); i++)
			{
				exponent_array.push_back(pow(frequency, -m_fractal_dimension.property_value()));
				frequency *= m_lacunarity.value();
			}

		k3d::mesh::points_t::iterator point = mesh->points.begin();
		for(unsigned long z = 0; z < points; z++)
			{
				for(unsigned long x = 0; x < points; x++)
					{
						// 5 is an arbitrary constant to keep same y-scale ratio as other terrain algorithms
						const double elevation = get_elevation(x, z, points, exponent_array, m_frequency.property_value(), m_noise_offset.property_value(), m_offset.property_value(), m_lacunarity.property_value(), m_octaves.property_value());

						(*point)->position.n[1] = elevation;
						point++;
					}
			}

		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_terrain_hfbm_implementation>, k3d::interface_list<k3d::imesh_source > > factory(
			k3d::uuid(0xff22f8f8, 0xa8b540f6, 0xb612a012, 0x8d4e9adb),
			"PolyTerrainHfBM",
			"Generates an HfBM-based fractal terrain",
			"Objects",
			k3d::iplugin_factory::EXPERIMENTAL);

		return factory;
	}

private:
	k3d_measurement_property(unsigned long, k3d::immutable_name, k3d::change_signal, k3d::with_undo, k3d::local_storage, k3d::with_constraint) m_iterations;
	k3d_measurement_property(double, k3d::immutable_name, k3d::change_signal, k3d::with_undo, k3d::local_storage, k3d::no_constraint) m_fractal_dimension;
	k3d_measurement_property(double, k3d::immutable_name, k3d::change_signal, k3d::with_undo, k3d::local_storage, k3d::no_constraint) m_lacunarity;
	k3d_measurement_property(double, k3d::immutable_name, k3d::change_signal, k3d::with_undo, k3d::local_storage, k3d::no_constraint) m_octaves;
	k3d_measurement_property(double, k3d::immutable_name, k3d::change_signal, k3d::with_undo, k3d::local_storage, k3d::no_constraint) m_offset;
	k3d_measurement_property(double, k3d::immutable_name, k3d::change_signal, k3d::with_undo, k3d::local_storage, k3d::no_constraint) m_frequency;
	k3d_measurement_property(double, k3d::immutable_name, k3d::change_signal, k3d::with_undo, k3d::local_storage, k3d::no_constraint) m_noise_offset;
};

/////////////////////////////////////////////////////////////////////////////
// poly_terrain_hfbm_factory

k3d::iplugin_factory& poly_terrain_hfbm_factory()
{
	return poly_terrain_hfbm_implementation::get_factory();
}

} // namespace libk3dmesh


