/* -*-c++-*- IfcQuery www.ifcquery.com
*
MIT License

Copyright (c) 2017 Fabian Gerold

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

#pragma once

#include <BRepAdaptor_Curve.hxx>
#include <BRep_Tool.hxx>
#include <GCPnts_AbscissaPoint.hxx>
#include <GCPnts_UniformAbscissa.hxx>
#include <Geom_Line.hxx>
#include <TopExp.hxx>
#include <TopExp_Explorer.hxx>
#include <TopoDS.hxx>
#include <TopoDS_Edge.hxx>
#include <TopoDS_Shape.hxx>
#include <TopoDS_Vertex.hxx>

#include <ifcpp/geometry/GeometrySettings.h>
#include <ifcpp/model/BuildingModel.h>
#include <ifcpp/model/BasicTypes.h>
#include <ifcpp/model/StatusCallback.h>
#include <ifcpp/IFC4/include/IfcCurtainWall.h>
#include <ifcpp/IFC4/include/IfcPropertySetDefinitionSet.h>
#include <ifcpp/IFC4/include/IfcRelAggregates.h>
#include <ifcpp/IFC4/include/IfcRelContainedInSpatialStructure.h>
#include <ifcpp/IFC4/include/IfcRelDefinesByProperties.h>
#include <ifcpp/IFC4/include/IfcSpace.h>
#include <ifcpp/IFC4/include/IfcWindow.h>

#include "RepresentationConverterOCC.h"
#include "GeometryInputDataOCC.h"

class GeometryConverterOCC : public StatusCallback
{
protected:
	shared_ptr<BuildingModel>						m_ifc_model;
	shared_ptr<GeometrySettings>					m_geom_settings;
	shared_ptr<RepresentationConverterOCC>			m_representation_converter;
	std::map<int, shared_ptr<ProductShapeDataOCC> >	m_product_shape_data;
	std::map<int, shared_ptr<BuildingObject> >		m_map_outside_spatial_structure;
	double											m_recent_progress;
	std::map<int, std::vector<shared_ptr<StatusCallback::Message> > > m_messages;
#ifdef ENABLE_OPENMP
	Mutex m_writelock_messages;
	Mutex m_writelock_appearance_cache;
#endif

public:
	// getters and setters
	shared_ptr<BuildingModel>&						getBuildingModel() { return m_ifc_model; }
	shared_ptr<RepresentationConverterOCC>&			getRepresentationConverter() { return m_representation_converter; }
	std::map<int, shared_ptr<ProductShapeDataOCC> >& getShapeInputData() { return m_product_shape_data; }
	shared_ptr<GeometrySettings>&					getGeomSettings() { return m_geom_settings; }
	std::map<int, shared_ptr<BuildingObject> >&		getObjectsOutsideSpatialStructure() { return m_map_outside_spatial_structure; }
	
	GeometryConverterOCC( shared_ptr<BuildingModel>& ifc_model )
	{
		m_ifc_model = ifc_model;
		m_geom_settings = shared_ptr<GeometrySettings>( new GeometrySettings() );
		resetNumVerticesPerCircle();
		shared_ptr<UnitConverter>& unit_converter = m_ifc_model->getUnitConverter();
		m_representation_converter = shared_ptr<RepresentationConverterOCC>( new RepresentationConverterOCC( m_geom_settings, unit_converter ) );
		
		// redirect all messages to this
		m_ifc_model->setMessageTarget( this );
		m_representation_converter->setMessageTarget( this );
	}
	virtual ~GeometryConverterOCC() {}

	void resetModel()
	{
		progressTextCallback( L"Unloading model, cleaning up memory..." );
		clearInputCache();
		m_recent_progress = 0.0;

		m_ifc_model->clearCache();
		m_ifc_model->clearIfcModel();
		progressTextCallback( L"Unloading model done" );
		progressValueCallback( 0.0, "parse" );

#ifdef _DEBUG
		//GeomDebugUtils::clearDebugDumpFile();
#endif
	}

	void clearInputCache()
	{
		m_product_shape_data.clear();
		m_map_outside_spatial_structure.clear();
		m_representation_converter->clearCache();
		m_messages.clear();
	}

	void resetNumVerticesPerCircle()
	{
		m_geom_settings->resetNumVerticesPerCircle();
	}

	void setModel( shared_ptr<BuildingModel> model )
	{
		if( m_ifc_model )
		{
			m_ifc_model->unsetMessageCallBack();
		}
		clearInputCache();
		m_ifc_model = model;
		m_representation_converter->clearCache();
		m_representation_converter->setUnitConverter( m_ifc_model->getUnitConverter() );
		m_ifc_model->setMessageTarget( this );
	}

	/*\brief method convertGeometry: Creates geometry for OpenCascade from previously loaded BuildingModel model.
	\param[out] parent_group Group to append the resulting geometry.
	**/
	void convertGeometry()
	{
		progressTextCallback( L"Creating geometry..." );
		progressValueCallback( 0, "geometry" );
		m_product_shape_data.clear();
		m_map_outside_spatial_structure.clear();
		m_representation_converter->clearCache();

		shared_ptr<ProductShapeDataOCC> ifc_project_data;
		std::vector<shared_ptr<IfcObjectDefinition> > vec_ifc_object_defs;
		const double length_to_meter_factor = m_ifc_model->getUnitConverter()->getLengthInMeterFactor();

		const std::map<int, shared_ptr<BuildingEntity> >& map_entities = m_ifc_model->getMapIfcEntities();
		for( auto it = map_entities.begin(); it != map_entities.end(); ++it )
		{
			shared_ptr<BuildingEntity> obj = it->second;
			shared_ptr<IfcObjectDefinition> object_def = dynamic_pointer_cast<IfcObjectDefinition>(obj);
			if( object_def )
			{
				vec_ifc_object_defs.push_back( object_def );
			}
		}

		// create geometry for for each IfcProduct independently, spatial structure will be resolved later
		std::map<int, shared_ptr<ProductShapeDataOCC> >* map_products_ptr = &m_product_shape_data;
		const int num_products = (int)vec_ifc_object_defs.size();

#ifdef ENABLE_OPENMP
		Mutex writelock_map;
		Mutex writelock_ifc_project;

#pragma omp parallel firstprivate(num_products) shared(map_products_ptr)
		{
			// time for one product may vary significantly, so schedule not so many
#pragma omp for schedule(dynamic,10)
#endif
			for( int i = 0; i < num_products; ++i )
			{
				shared_ptr<IfcObjectDefinition> ifc_object_def = vec_ifc_object_defs[i];
				const int product_id = ifc_object_def->m_entity_id;
				shared_ptr<ProductShapeDataOCC> product_geom_input_data( new ProductShapeDataOCC( product_id ) );
				product_geom_input_data->m_ifc_object_definition = ifc_object_def;

				std::stringstream thread_err;
				if( dynamic_pointer_cast<IfcFeatureElementSubtraction>(ifc_object_def) )
				{
					// geometry will be created in method subtractOpenings
					continue;
				}
				else if( dynamic_pointer_cast<IfcProject>(ifc_object_def) )
				{
#ifdef ENABLE_OPENMP
					ScopedLock scoped_lock( writelock_ifc_project );
#endif
					ifc_project_data = product_geom_input_data;
				}

				try
				{
					convertIfcProductShape( product_geom_input_data );
				}
				catch( OutOfMemoryException& e )
				{
					throw e;
				}
				catch( BuildingException& e )
				{
					thread_err << e.what();
				}
				catch( Standard_Failure& sf )
				{
					thread_err << sf.GetMessageString();
				}
				catch( std::exception& e )
				{
					thread_err << e.what();
				}
				catch( ... )
				{
					thread_err << "undefined error, product id " << product_id;
				}

				{
#ifdef ENABLE_OPENMP
					ScopedLock scoped_lock( writelock_map );
#endif
					map_products_ptr->insert( std::make_pair( product_id, product_geom_input_data ) );

					if( thread_err.tellp() > 0 )
					{
						messageCallback( thread_err.str().c_str(), StatusCallback::MESSAGE_TYPE_ERROR, __FUNC__ );
					}
				}

				// progress callback
				double progress = (double)i / (double)num_products;
				if( progress - m_recent_progress > 0.02 )
				{

#ifdef ENABLE_OPENMP
					if( omp_get_thread_num() == 0 )
#endif
					{
						// leave 10% of progress to openscenegraph internals
						progressValueCallback( progress*0.9, "geometry" );
						m_recent_progress = progress;
					}
				}
			}
#ifdef ENABLE_OPENMP
		} // implicit barrier
#endif

		try
		{
			// now resolve spatial structure
			if( ifc_project_data )
			{
				resolveProjectStructure( ifc_project_data );
			}

			// check if there are entities that are not in spatial structure
			for( auto it_product_shapes = m_product_shape_data.begin(); it_product_shapes != m_product_shape_data.end(); ++it_product_shapes )
			{
				shared_ptr<ProductShapeDataOCC> product_shape = it_product_shapes->second;
				if( !product_shape )
				{
					continue;
				}

				if( !product_shape->m_added_to_spatial_structure )
				{
					if( !product_shape->m_ifc_object_definition.expired() )
					{
						shared_ptr<IfcObjectDefinition> ifc_product( product_shape->m_ifc_object_definition );
						shared_ptr<IfcFeatureElementSubtraction> opening = dynamic_pointer_cast<IfcFeatureElementSubtraction>(ifc_product);
						if( opening )
						{
							continue;
						}
						m_map_outside_spatial_structure[ifc_product->m_entity_id] = ifc_product;
					}
				}
			}
		}
		catch( OutOfMemoryException& e )
		{
			throw e;
		}
		catch( BuildingException& e )
		{
			messageCallback( e.what(), StatusCallback::MESSAGE_TYPE_ERROR, "" );
		}
		catch( std::exception& e )
		{
			messageCallback( e.what(), StatusCallback::MESSAGE_TYPE_ERROR, "" );
		}
		catch( ... )
		{
			messageCallback( "undefined error", StatusCallback::MESSAGE_TYPE_ERROR, __FUNC__ );
		}

		m_representation_converter->getProfileCache()->clearProfileCache();
		progressTextCallback( L"Loading file done" );
		progressValueCallback( 1.0, "geometry" );
	}

	//\brief method convertIfcProduct: Creates geometry objects (meshset with connected vertex-edge-face graph) from an IfcProduct object
	// caution: when using OpenMP, this method runs in parallel threads, so every write access to member variables needs a write lock
	void convertIfcProductShape( shared_ptr<ProductShapeDataOCC>& product_shape )
	{
		if( product_shape->m_ifc_object_definition.expired() )
		{
			return;
		}
		shared_ptr<IfcObjectDefinition> ifc_object_def( product_shape->m_ifc_object_definition );
		shared_ptr<IfcProduct> ifc_product = dynamic_pointer_cast<IfcProduct>(ifc_object_def);
		if( !ifc_product )
		{
			return;
		}
		if( !ifc_product->m_Representation )
		{
			return;
		}

		// evaluate IFC geometry
		const double length_factor = m_ifc_model->getUnitConverter()->getLengthInMeterFactor();
		shared_ptr<IfcProductRepresentation>& product_representation = ifc_product->m_Representation;
		std::vector<shared_ptr<IfcRepresentation> >& vec_representations = product_representation->m_Representations;
		for( size_t i_representations = 0; i_representations < vec_representations.size(); ++i_representations )
		{
			const shared_ptr<IfcRepresentation>& representation = vec_representations[i_representations];
			try
			{
				shared_ptr<RepresentationDataOCC> representation_data( new RepresentationDataOCC() );
				m_representation_converter->convertIfcRepresentation( representation, representation_data );
				product_shape->m_vec_representations.push_back( representation_data );
			}
			catch( OutOfMemoryException& e )
			{
				throw e;
			}
			catch( BuildingException& e )
			{
				messageCallback( e.what(), StatusCallback::MESSAGE_TYPE_ERROR, "" );
			}
			catch( std::exception& e )
			{
				messageCallback( e.what(), StatusCallback::MESSAGE_TYPE_ERROR, "" );
			}
		}

		// IfcProduct has an ObjectPlacement that can be local or global
		gp_Trsf product_placement_matrix;
		if( ifc_product->m_ObjectPlacement )
		{
			// IfcPlacement2Matrix follows related placements in case of local coordinate systems
			std::unordered_set<IfcObjectPlacement*> placement_already_applied;
			PlacementConverterOCC::convertIfcObjectPlacement( ifc_product->m_ObjectPlacement, length_factor, product_placement_matrix, this, placement_already_applied );
			product_shape->applyTransformToProduct( product_placement_matrix );
		}

		// handle openings
		std::vector<shared_ptr<ProductShapeDataOCC> > vec_opening_data;
		const shared_ptr<IfcElement> ifc_element = dynamic_pointer_cast<IfcElement>(ifc_product);
		if( ifc_element )
		{
			m_representation_converter->subtractOpenings( ifc_element, product_shape );
		}

		// Fetch the IFCProduct relationships
		if( ifc_product->m_IsDefinedBy_inverse.size() > 0 )
		{
			std::vector<weak_ptr<IfcRelDefinesByProperties> >& vec_IsDefinedBy_inverse = ifc_product->m_IsDefinedBy_inverse;
			for( size_t i = 0; i < vec_IsDefinedBy_inverse.size(); ++i )
			{
				shared_ptr<IfcRelDefinesByProperties> rel_def( vec_IsDefinedBy_inverse[i] );
				shared_ptr<IfcPropertySetDefinitionSelect> relating_property_definition_select = rel_def->m_RelatingPropertyDefinition;
				if( relating_property_definition_select )
				{
					// TYPE IfcPropertySetDefinitionSelect = SELECT	(IfcPropertySetDefinition	,IfcPropertySetDefinitionSet);
					shared_ptr<IfcPropertySetDefinition> property_set_def = dynamic_pointer_cast<IfcPropertySetDefinition>(relating_property_definition_select);
					if( property_set_def )
					{
						shared_ptr<IfcPropertySet> property_set = dynamic_pointer_cast<IfcPropertySet>(property_set_def);
						if( property_set )
						{
							readAppearanceFromPropertySet( property_set, product_shape );
						}
						continue;
					}

					shared_ptr<IfcPropertySetDefinitionSet> property_set_def_set = dynamic_pointer_cast<IfcPropertySetDefinitionSet>(relating_property_definition_select);
					if( property_set_def_set )
					{
						std::vector<shared_ptr<IfcPropertySetDefinition> >& vec_propterty_set_def = property_set_def_set->m_vec;
						std::vector<shared_ptr<IfcPropertySetDefinition> >::iterator it_property_set_def;
						for( it_property_set_def = vec_propterty_set_def.begin(); it_property_set_def != vec_propterty_set_def.end(); ++it_property_set_def )
						{
							shared_ptr<IfcPropertySetDefinition> property_set_def2 = (*it_property_set_def);
							if( property_set_def2 )
							{
								shared_ptr<IfcPropertySet> property_set = dynamic_pointer_cast<IfcPropertySet>(property_set_def2);
								if( property_set )
								{
									readAppearanceFromPropertySet( property_set, product_shape );
								}
							}
						}
						continue;
					}
				}
			}
		}
	}

	void readAppearanceFromPropertySet( const shared_ptr<IfcPropertySet>& prop_set, shared_ptr<ProductShapeDataOCC>& product_shape )
	{
		if( !prop_set )
		{
			return;
		}
		for( auto& ifc_property : prop_set->m_HasProperties )
		{
			if( !ifc_property )
			{
				continue;
			}

			shared_ptr<IfcSimpleProperty> simple_property = dynamic_pointer_cast<IfcSimpleProperty>(ifc_property);
			if( simple_property )
			{
				// ENTITY IfcSimpleProperty ABSTRACT SUPERTYPE OF(ONEOF( IfcPropertyBoundedValue, IfcPropertyEnumeratedValue, IfcPropertyListValue,
				// IfcPropertyReferenceValue, IfcPropertySingleValue, IfcPropertyTableValue))

				shared_ptr<IfcIdentifier> property_name = simple_property->m_Name;
				std::wstring name_str = property_name->m_value;
				if( name_str.compare( L"LayerName" ) == 0 )
				{
					// TODO: implement layers
				}
				shared_ptr<IfcText> description = simple_property->m_Description;


				shared_ptr<IfcPropertySingleValue> property_single_value = dynamic_pointer_cast<IfcPropertySingleValue>(simple_property);
				if( property_single_value )
				{
					//shared_ptr<IfcValue>& nominal_value = property_single_value->m_NominalValue;				//optional
					//shared_ptr<IfcUnit>& unit = property_single_value->m_Unit;						//optional

				}

				continue;
			}

			shared_ptr<IfcComplexProperty> complex_property = dynamic_pointer_cast<IfcComplexProperty>(ifc_property);
			if( complex_property )
			{
				if( !complex_property->m_UsageName ) continue;
				if( complex_property->m_UsageName->m_value.compare( L"Color" ) == 0 )
				{
					vec4 vec_color;
					m_representation_converter->getStylesConverter()->convertIfcComplexPropertyColor( complex_property, vec_color );
					shared_ptr<AppearanceData> appearance_data( new AppearanceData( -1 ) );
					if( !appearance_data )
					{
						throw OutOfMemoryException( __FUNC__ );
					}
					appearance_data->m_apply_to_geometry_type = AppearanceData::GEOM_TYPE_ANY;
					appearance_data->m_color_ambient = vec_color;
					appearance_data->m_color_diffuse = vec_color;
					appearance_data->m_color_specular = vec_color;
					appearance_data->m_shininess = 35.f;
					product_shape->addAppearance( appearance_data );
				}
			}
		}
	}

	void resolveProjectStructure( shared_ptr<ProductShapeDataOCC>& product_data )
	{
		if( !product_data )
		{
			return;
		}
		if( product_data->m_ifc_object_definition.expired() )
		{
			return;
		}
		product_data->m_added_to_spatial_structure = true;
		shared_ptr<IfcObjectDefinition> ifc_object_def( product_data->m_ifc_object_definition );
		const int entity_id = ifc_object_def->m_entity_id;

		const std::vector<weak_ptr<IfcRelAggregates> >& vec_IsDecomposedBy = ifc_object_def->m_IsDecomposedBy_inverse;
		for( size_t ii = 0; ii < vec_IsDecomposedBy.size(); ++ii )
		{
			const weak_ptr<IfcRelAggregates>& rel_aggregates_weak_ptr = vec_IsDecomposedBy[ii];
			if( rel_aggregates_weak_ptr.expired() )
			{
				continue;
			}
			shared_ptr<IfcRelAggregates> rel_aggregates( rel_aggregates_weak_ptr );
			if( rel_aggregates )
			{
				const std::vector<shared_ptr<IfcObjectDefinition> >& vec_related_objects = rel_aggregates->m_RelatedObjects;
				for( size_t jj = 0; jj < vec_related_objects.size(); ++jj )
				{
					const shared_ptr<IfcObjectDefinition>& related_obj_def = vec_related_objects[jj];
					if( related_obj_def )
					{
						auto it_product_map = m_product_shape_data.find( related_obj_def->m_entity_id );
						if( it_product_map != m_product_shape_data.end() )
						{
							shared_ptr<ProductShapeDataOCC>& related_product_shape = it_product_map->second;
							if( related_product_shape )
							{
								product_data->addChildProduct( related_product_shape, product_data );
								resolveProjectStructure( related_product_shape );
							}
						}
					}
				}
			}
		}

		shared_ptr<IfcSpatialStructureElement> spatial_ele = dynamic_pointer_cast<IfcSpatialStructureElement>(ifc_object_def);
		if( spatial_ele )
		{
			const std::vector<weak_ptr<IfcRelContainedInSpatialStructure> >& vec_contains = spatial_ele->m_ContainsElements_inverse;
			for( size_t ii = 0; ii < vec_contains.size(); ++ii )
			{
				const weak_ptr<IfcRelContainedInSpatialStructure>& rel_contained_weak_ptr = vec_contains[ii];
				if( rel_contained_weak_ptr.expired() )
				{
					continue;
				}
				shared_ptr<IfcRelContainedInSpatialStructure> rel_contained( rel_contained_weak_ptr );
				if( rel_contained )
				{
					const std::vector<shared_ptr<IfcProduct> >& vec_related_elements = rel_contained->m_RelatedElements;

					for( size_t jj = 0; jj < vec_related_elements.size(); ++jj )
					{
						const shared_ptr<IfcProduct>& related_product = vec_related_elements[jj];
						if( related_product )
						{
							auto it_product_map = m_product_shape_data.find( related_product->m_entity_id );
							if( it_product_map != m_product_shape_data.end() )
							{
								shared_ptr<ProductShapeDataOCC>& related_product_shape = it_product_map->second;
								if( related_product_shape )
								{
									product_data->addChildProduct( related_product_shape, product_data );
									resolveProjectStructure( related_product_shape );
								}
							}
						}
					}
				}
			}
		}

		// TODO: handle IfcRelAssignsToProduct
	}
	
	virtual void messageTarget( void* ptr, shared_ptr<StatusCallback::Message> m )
	{
		GeometryConverterOCC* myself = (GeometryConverterOCC*)ptr;
		if( myself )
		{
			if( m->m_entity )
			{
	#ifdef ENABLE_OPENMP
				ScopedLock lock( myself->m_writelock_messages );
	#endif

				// make sure that the same message for one entity does not appear several times
				const int entity_id = m->m_entity->m_entity_id;

				auto it = myself->m_messages.find( entity_id );
				if( it != myself->m_messages.end() )
				{
					std::vector<shared_ptr<StatusCallback::Message> >& vec_message_for_entity = it->second;
					for( size_t i = 0; i < vec_message_for_entity.size(); ++i )
					{
						shared_ptr<StatusCallback::Message>& existing_message = vec_message_for_entity[i];
						if( existing_message->m_message_text.compare( m->m_message_text ) == 0 )
						{
							// same message for same entity is already there, so ignore message
							return;
						}
					}
					vec_message_for_entity.push_back( m );
				}
				else
				{
					std::vector<shared_ptr<StatusCallback::Message> >& vec = myself->m_messages.insert( std::make_pair( entity_id, std::vector<shared_ptr<StatusCallback::Message> >() ) ).first->second;
					vec.push_back( m );
				}
			}

			myself->messageCallback( m );
		}
	}
};
