#include "ShapeAlgorithms.h"
#include <TopExp_Explorer.hxx>
#include <BRep_Tool.hxx>
#include <TopoDS.hxx>
#include <TopoDS_Vertex.hxx>
#include <GProp_GProps.hxx>
#include <BRepGProp.hxx>
#include <BRepAlgoAPI_Common.hxx>
#include <Bnd_Box.hxx>
#include <BRepBndLib.hxx>
#include <BRepPrimAPI_MakeSphere.hxx>
#include <BRepBuilderAPI_MakeFace.hxx>
#include <BRepBuilderAPI_MakeEdge.hxx>
#include <GC_MakeSegment.hxx>
#include <BRepPrimAPI_MakeBox.hxx>
#include <BRepAlgoAPI_Cut.hxx>
#include <BRepTools.hxx>
#include <STEPControl_Writer.hxx>
#include <STEPControl_Reader.hxx>
#include <IGESControl_Reader.hxx>
#include <BRepExtrema_DistShapeShape.hxx>
#include <BRepBuilderAPI_MakeWire.hxx>
#include <BRepMesh_IncrementalMesh.hxx>
#include <BOPAlgo_Options.hxx>
#include <BOPTools_AlgoTools3D.hxx>
#include <BRepPrimAPI_MakeHalfSpace.hxx>
#include <BRepPrimAPI_MakeRevol.hxx>
#include <GeomLib_IsPlanarSurface.hxx>
#include <BRepBuilderAPI_MakeVertex.hxx>
#include <BRepBuilderAPI_Transform.hxx>
#include <BRepBuilderAPI_Copy.hxx>
#include <BRepTools_ReShape.hxx>
#include <GeomLProp_SLProps.hxx>
#include <Interface_Static.hxx>
#include <IGESData_IGESmodel.hxx>
#include <IGESData_IGESEntity.hxx>
#include <IGESData_IGESType.hxx>
#include <ShapeFix_Wireframe.hxx>
#include "nlohmann/json.hpp"
#include "shapePartition.h"
#include "easylogging++/easylogging++.h"
#include "misc/miscFunctions.hpp"
#include "cenos_exception.h"

using json = nlohmann::json;



/// <summary>
/// Compares if two shapes have equal shapes of highest type
/// </summary>
/// <param name="s1"></param>
/// <param name="s2"></param>
/// <returns></returns>
bool ShapeAlgorithms::have_equal_shapes(const TopoDS_Shape& s1, const TopoDS_Shape& s2)
{
	// false if any of shapes is null
	if (s1.IsNull() || s2.IsNull())
	{
		return false;
	}

	//if shape 1 is compound, explore it for highest shape and check 
	if (s1.ShapeType() == TopAbs_COMPOUND)
	{
		TopTools_ListOfShape shape_list;
		for (TopExp_Explorer explorer(s1, getHighestShapeType(s1)); explorer.More(); explorer.Next())
		{
			shape_list.Append(explorer.Current());
		}
		return have_equal_shapes(s2, shape_list);
	}

	//if shape 2 is compound, explore it for highest shape and check 
	else if (s2.ShapeType() == TopAbs_COMPOUND)
	{
		TopTools_ListOfShape shape_list;
		for (TopExp_Explorer explorer(s2, getHighestShapeType(s2)); explorer.More(); explorer.Next())
		{
			shape_list.Append(explorer.Current());
		}
		return have_equal_shapes(s1, shape_list);
	}

	//if both were compounds, it is possible that shape types are different
	else if (s1.ShapeType() != s2.ShapeType())
		return false;

	else if (s1.ShapeType() == TopAbs_SOLID)
		return compare_solids(s1, s2);

	else if (s1.ShapeType() == TopAbs_FACE)
		return compare_faces(s1, s2);

	else if (s1.ShapeType() == TopAbs_VERTEX)
		return compare_vertices(s1, s2);

	else if (s1.ShapeType() == TopAbs_EDGE)
		return compare_edges(s1, s2);

	else
		return false;

}

bool ShapeAlgorithms::have_equal_shapes(const TopoDS_Shape& s1, const TopTools_ListOfShape& l2)
{
	for (const TopoDS_Shape& s2 : l2)
	{
		if (have_equal_shapes(s1, s2))
			return true;
	}
	return false;
}

bool ShapeAlgorithms::are_equal(const TopoDS_Shape& s1, const TopoDS_Shape& s2)
{
	// false if any of shapes is null
	if (s1.IsNull() || s2.IsNull())
	{
		return false;
	}

	else if (s1.ShapeType() == TopAbs_SOLID)
		return compare_solids(s1, s2);

	else if (s1.ShapeType() == TopAbs_FACE)
		return compare_faces(s1, s2);

	else if (s1.ShapeType() == TopAbs_VERTEX)
		return compare_vertices(s1, s2);

	else if (s1.ShapeType() == TopAbs_EDGE)
		return compare_edges(s1, s2);

	else
		return false;

}

bool ShapeAlgorithms::are_equal(const TopoDS_Shape& s1, const TopTools_ListOfShape& l2)
{
	for (const TopoDS_Shape& s2 : l2)
	{
		if (are_equal(s1, s2))
			return true;
	}
	return false;
}

bool ShapeAlgorithms::have_common(const TopoDS_Shape& s1, const TopoDS_Shape& s2)
{
	// false if any of shapes is null
	if (s1.IsNull() || s2.IsNull())
	{
		return false;
	}

	//if shape 1 is compound, explore it for highest shape and check 
	if (s1.ShapeType() == TopAbs_COMPOUND)
	{
		TopTools_ListOfShape shape_list;
		for (TopExp_Explorer explorer(s1, getHighestShapeType(s1)); explorer.More(); explorer.Next())
		{
			shape_list.Append(explorer.Current());
		}
		return have_common(s2, shape_list);

	}

	/*	if (s1.ShapeType() != s2.ShapeType())
		{
			return false;
		}*/

	if (s1.ShapeType() == TopAbs_SOLID)
		return have_common_solids(s1, s2);

	if (s1.ShapeType() == TopAbs_FACE)
		return have_common_faces(s1, s2);

	if (s1.ShapeType() == TopAbs_EDGE)
		return have_common_edges(s1, s2);
	return false;
}

bool ShapeAlgorithms::have_common(const TopoDS_Shape& s1, const TopTools_ListOfShape& l2)
{
	for (const TopoDS_Shape& s2 : l2)
	{
		if (have_common(s1, s2))
			return true;
	}
	return false;
}

bool ShapeAlgorithms::compare_bounding_boxes(const TopoDS_Shape s1, const TopoDS_Shape s2)
{
	// BRepBndLib::Add() by default uses triangulation data stored in the shape,
	// so that the result will depend on the presence of triangulation and on it's quality.
	// 
	// AddOptimal can be used instead to re-triangulate the geometry (as BRepMesh_IncrementalMesh).

	Bnd_Box B1;
	BRepBndLib::Add(s1, B1);
	double Xmin1, Ymin1, Zmin1, Xmax1, Ymax1, Zmax1;
	B1.Get(Xmin1, Ymin1, Zmin1, Xmax1, Ymax1, Zmax1);

	Bnd_Box B2;
	BRepBndLib::Add(s2, B2);
	double Xmin2, Ymin2, Zmin2, Xmax2, Ymax2, Zmax2;
	B2.Get(Xmin2, Ymin2, Zmin2, Xmax2, Ymax2, Zmax2);
	
	double tolerance = 0.04 *
		std::max(std::max(std::max(Xmax1 - Xmin1, Xmax2 - Xmin2), std::max(Ymax1 - Ymin1, Ymax2 - Ymin2)), std::max(Zmax1 - Zmin1, Zmax2 - Zmin2));

	if ((double)fabs(Xmin1 - Xmin2) < tolerance &&
		(double)fabs(Xmax1 - Xmax2) < tolerance &&
		(double)fabs(Ymin1 - Ymin2) < tolerance &&
		(double)fabs(Ymax1 - Ymax2) < tolerance &&
		(double)fabs(Zmin1 - Zmin2) < tolerance &&
		(double)fabs(Zmax1 - Zmax2) < tolerance)
		return true;

	return false;
}


void tessellate(TopoDS_Shape shape) {
	// Tesselate the shape. The new tesselation is stored in the shape object.

	// Tessellating degenerated edge results in a crash, so this must not be done.
	if (shape.ShapeType() == TopAbs_ShapeEnum::TopAbs_EDGE) {
		if (BRep_Tool::Degenerated(TopoDS::Edge(shape))) {
			return;
		}
	}

	// Linear deflection multiplier for the bounding box max size (0.1%)
	double relativeDeflection = 0.001;

	Bnd_Box box;
	BRepBndLib::Add(shape, box);
	double aXmin, aYmin, aZmin, aXmax, aYmax, aZmax;
	box.Get(aXmin, aYmin, aZmin, aXmax, aYmax, aZmax);
	double deflection;
	deflection = std::max(std::max(aXmax - aXmin, aYmax - aYmin), aZmax - aZmin) * relativeDeflection;
	BRepMesh_IncrementalMesh Inc(shape, deflection);
}
bool ShapeAlgorithms::compare_solids(const TopoDS_Shape s1, const TopoDS_Shape s2)
{
	// Tessellation must be performed, because otherwise surface/volume 
	// properties and bounding box can be wrong for complex shapes.
	tessellate(s1);
	tessellate(s2);

	GProp_GProps gprops1, gprops2;
	BRepGProp::VolumeProperties(s1, gprops1);
	BRepGProp::VolumeProperties(s2, gprops2);

	// Square root of the area yields approximate linear lenght.
	double length_factor = std::pow(gprops1.Mass(), 1.0 / 3);
	//compare by volume with tolerance
	if ((double)(fabs(gprops1.Mass() - gprops2.Mass()) > (double)0.001 * gprops1.Mass()))
		return false;

	//compare by center of mass
	gp_Pnt center1 = gprops1.CentreOfMass();
	if (!center1.IsEqual(gprops2.CentreOfMass(), (double)0.001 * length_factor))
		return false;

	if (!compare_bounding_boxes(s1, s2))
		return false;

	return true;
}

bool ShapeAlgorithms::compare_faces(const TopoDS_Shape s1, const TopoDS_Shape s2)
{
	// Tessellation must be performed, because otherwise surface/volume 
	// properties and bounding box can be wrong for complex shapes.
	tessellate(s1);
	tessellate(s2);

	GProp_GProps gprops1, gprops2;
	BRepGProp::SurfaceProperties(s1, gprops1);
	BRepGProp::SurfaceProperties(s2, gprops2);

	// Compare by area with tolerance.
	if ((double)(fabs(gprops1.Mass() - gprops2.Mass()) > (double)0.001 * gprops1.Mass())) {
		return false;
	}

	// Compare by center of mass.
	// Square root of the area yields approximate linear lenght.
	double length_factor = std::pow(gprops1.Mass(), 1.0 / 2);
	gp_Pnt center1 = gprops1.CentreOfMass();
	if (!center1.IsEqual(gprops2.CentreOfMass(), 0.001 * length_factor)) {
		return false;
	}

	if (!compare_bounding_boxes(s1, s2)) {
		return false;
	}

	return true;
}


bool ShapeAlgorithms::compare_edges(const TopoDS_Shape s1, const TopoDS_Shape s2)
{
	// Tessellation must be performed, because otherwise surface/volume 
	// properties and bounding box can be wrong for complex shapes.
	tessellate(s1);
	tessellate(s2);

	GProp_GProps gprops1, gprops2;
	BRepGProp::LinearProperties(s1, gprops1);
	BRepGProp::LinearProperties(s2, gprops2);

	double length_factor = gprops1.Mass();
	//compare by length with tolerance
	if ((double)(fabs(gprops1.Mass() - gprops2.Mass()) > (double)0.001 * length_factor)	)
		return false;

	//compare by center of mass
	gp_Pnt center1 = gprops1.CentreOfMass();
	if (!center1.IsEqual(gprops2.CentreOfMass(), 0.001 * length_factor))
		return false;

	if (!compare_bounding_boxes(s1, s2))
		return false;

	return true;
}



bool ShapeAlgorithms::compare_vertices(const TopoDS_Shape s1, const TopoDS_Shape s2)
{
	gp_Pnt P1 = BRep_Tool::Pnt(TopoDS::Vertex(s1));
	gp_Pnt P2 = BRep_Tool::Pnt(TopoDS::Vertex(s2));
	return P1.IsEqual(P2, 1e-6);
}

//returns true, if there is at lease one solid after common boolean
bool ShapeAlgorithms::have_common_solids(const TopoDS_Shape s1, const TopoDS_Shape s2)
{
	BRepAlgoAPI_Common common_builder;
	common_builder.SetRunParallel(true);
	TopTools_ListOfShape s1_list, s2_list;
	s1_list.Append(s1);
	s2_list.Append(s2);
	common_builder.SetArguments(s1_list);
	common_builder.SetTools(s2_list);
	common_builder.Build();

	TopoDS_Shape common = common_builder.Shape();
	for (TopExp_Explorer solidExplorer(common, TopAbs_ShapeEnum::TopAbs_SOLID); solidExplorer.More(); solidExplorer.Next())
	{
		return true;
	}

	return false;
}


//returns true, if there is at lease one face after common boolean
bool ShapeAlgorithms::have_common_faces(const TopoDS_Shape s1, const TopoDS_Shape s2)
{
	BRepAlgoAPI_Common common_builder;
	common_builder.SetRunParallel(true);
	TopTools_ListOfShape s1_list, s2_list;
	s1_list.Append(s1);
	s2_list.Append(s2);
	common_builder.SetArguments(s1_list);
	common_builder.SetTools(s2_list);
	common_builder.Build();

	TopoDS_Shape common = common_builder.Shape();
	for (TopExp_Explorer faceExplorer(common, TopAbs_ShapeEnum::TopAbs_FACE); faceExplorer.More(); faceExplorer.Next())
	{
		return true;
	}

	return false;
}

//returns true, if there is at lease one edge after common boolean
bool ShapeAlgorithms::have_common_edges(const TopoDS_Shape s1, const TopoDS_Shape s2)
{
	BRepAlgoAPI_Common common_builder;
	common_builder.SetRunParallel(true);
	TopTools_ListOfShape s1_list, s2_list;
	s1_list.Append(s1);
	s2_list.Append(s2);
	common_builder.SetArguments(s1_list);
	common_builder.SetTools(s2_list);
	common_builder.Build();

	TopoDS_Shape common = common_builder.Shape();

	for (TopExp_Explorer edgeExplorer(common, TopAbs_ShapeEnum::TopAbs_EDGE); edgeExplorer.More(); edgeExplorer.Next())
	{
		return true;
	}
	
	return false;
}

double getAxialVolume(const TopoDS_Shape* shape, const double centerZ) {
	// Compute the axial volume around the Y axis (for 2D axially symmetric cases).

	double totalVolume = 0;

	for (TopExp_Explorer exp(*shape, TopAbs_ShapeEnum::TopAbs_FACE); exp.More(); exp.Next())
	{
		// The shape has to be iterated because it can be a compound consisting of multiple faces.
		// if a compound is passed to BRep_Tool::Surface(TopoDS::Face(sh)), it will throw Standard_TypeMismach exception.
		TopoDS_Shape sh = exp.Current();
			
		Handle(Geom_Surface) surface = BRep_Tool::Surface(TopoDS::Face(sh));
		GeomLib_IsPlanarSurface planarCheck(surface);
		gp_Dir dir;
		BOPTools_AlgoTools3D::GetNormalToSurface(surface, 0, 0, dir);
		gp_Dir zAxis(0, 0, 1);

		if (planarCheck.IsPlanar() and centerZ == 0.0 and (dir.IsEqual(zAxis, 0.05) or dir.IsOpposite(zAxis, 0.05))) {
			// For the face to be axially symmetric, it must be 2D (planar), on the XY plane (Z=0 and normal = Y axis).

			try {
				// Because the revolution is not a trivial task, and OCC can sometimes 
				// produce hard to track down exceptions, this is in try catch block.
				gp_Pnt origin(0, 0, 0);
				gp_Dir yAxis(0, 1, 0);
				TopoDS_Shape revolvedShape = BRepPrimAPI_MakeRevol(sh, gp_Ax1(origin, yAxis)).Shape();
				GProp_GProps volProps;
				BRepGProp::VolumeProperties(revolvedShape, volProps);
				totalVolume += volProps.Mass();
			}
			catch (...) {
				// TODO: Axial volume should only be calculated for axially symmetric cases. CP-1508
				LOG(ERROR) << "Could not compute axial volume for shape during getShapeStatistics";
				//throw cenos_exception("Could not compute axial volume for shape during getShapeStatistics");
			}
		}
	}

	return totalVolume;
}

void ShapeAlgorithms::getShapeStatistics(const TopoDS_Shape* shape, ShapeStats& stats)
{
	//  ShapeStats initializes all variables to zero

	auto highest_shape = getHighestShapeType(*shape);
	if (highest_shape == TopAbs_VERTEX)
	{
		auto exp = TopExp_Explorer(*shape, TopAbs_ShapeEnum::TopAbs_VERTEX);
		gp_Pnt pnt = BRep_Tool::Pnt(TopoDS::Vertex(exp.Current()));
		stats.centerX = pnt.X();
		stats.centerY = pnt.Y();
		stats.centerZ = pnt.Z();
		return;
	}

	else if (highest_shape == TopAbs_EDGE)
	{
		GProp_GProps lineProps;
		BRepGProp::LinearProperties(*shape, lineProps);
		gp_Pnt pc = lineProps.CentreOfMass();
		stats.centerX = pc.X();
		stats.centerY = pc.Y();
		stats.centerZ = pc.Z();
		stats.length = lineProps.Mass();
	}

	else if (highest_shape == TopAbs_FACE)
	{
		GProp_GProps shellProps;
		BRepGProp::SurfaceProperties(*shape, shellProps);
		gp_Pnt pc = shellProps.CentreOfMass();
		stats.centerX = pc.X();
		stats.centerY = pc.Y();
		stats.centerZ = pc.Z();
		stats.area = shellProps.Mass();
		stats.axiVolume = getAxialVolume(shape, stats.centerZ);
	}

	else if (highest_shape == TopAbs_SOLID)
	{
		GProp_GProps volProps;
		BRepGProp::VolumeProperties(*shape, volProps);
		gp_Pnt pc = volProps.CentreOfMass();
		stats.centerX = pc.X();
		stats.centerY = pc.Y();
		stats.centerZ = pc.Z();
		stats.volume = volProps.Mass();
	}

	try {
		Bnd_Box box;
		box.SetGap(0.0);
		BRepBndLib::AddOptimal(*shape, box, true);

		stats.maxX = box.CornerMax().X();
		stats.maxY = box.CornerMax().Y();
		stats.maxZ = box.CornerMax().Z();
		stats.minX = box.CornerMin().X();
		stats.minY = box.CornerMin().Y();
		stats.minZ = box.CornerMin().Z();
	}
	catch (...){
		throw cenos_exception("Could not compute bounding box for shape during getShapeStatistics");
        // This can happen due to some invalid geometried for which OCC cannot compute a bounding box
	}
}


TopAbs_ShapeEnum ShapeAlgorithms::getHighestShapeType(const TopoDS_Shape& sh)
{
	if (sh.ShapeType() == TopAbs_EDGE)
		return TopAbs_EDGE;
	else if (sh.ShapeType() == TopAbs_FACE)
		return TopAbs_FACE;
	else if (sh.ShapeType() == TopAbs_SOLID)
		return TopAbs_SOLID;
	else if (sh.ShapeType() == TopAbs_COMPOUND)
	{
		for (TopExp_Explorer explorer(sh, TopAbs_ShapeEnum::TopAbs_SOLID); explorer.More(); explorer.Next())
		{
			return TopAbs_SOLID;
		}

		for (TopExp_Explorer explorer(sh, TopAbs_ShapeEnum::TopAbs_FACE); explorer.More(); explorer.Next())
		{
			return TopAbs_FACE;
		}

		for (TopExp_Explorer explorer(sh, TopAbs_ShapeEnum::TopAbs_EDGE); explorer.More(); explorer.Next())
		{
			return TopAbs_EDGE;
		}

		return TopAbs_VERTEX;
	}

	return TopAbs_VERTEX;
}



bool ShapeAlgorithms::sameType(const TopoDS_Shape s1, const TopoDS_Shape s2)
{
	if (getHighestShapeType(s1) == getHighestShapeType(s2))
		return true;
	else
		return false;
}

bool ShapeAlgorithms::have_common_vertices(const TopoDS_Shape& s1, const TopoDS_Shape& s2)
{
	for (TopExp_Explorer explorer1(s1, TopAbs_ShapeEnum::TopAbs_VERTEX); explorer1.More(); explorer1.Next())
	{
		for (TopExp_Explorer explorer2(s2, TopAbs_ShapeEnum::TopAbs_VERTEX); explorer2.More(); explorer2.Next())
		{
			if (compare_vertices(explorer1.Current(), explorer2.Current()))
				return true;
		}
	}
	return false;
}

bool ShapeAlgorithms::isDegenerate(const TopoDS_Shape& s1)
{

	if (getHighestShapeType(s1) == TopAbs_EDGE)
	{
		GProp_GProps gprops;
		BRepGProp::LinearProperties(s1, gprops);
		if (gprops.Mass() < 1e-8)
			return true;
		else
			return false;
	}
	if (getHighestShapeType(s1) == TopAbs_FACE)
	{
		GProp_GProps gprops;
		BRepGProp::SurfaceProperties(s1, gprops);
		if (gprops.Mass() < 1e-8)
			return true;
		else
			return false;
	}
	if (getHighestShapeType(s1) == TopAbs_SOLID)
	{
		GProp_GProps gprops;
		BRepGProp::VolumeProperties(s1, gprops);
		if (gprops.Mass() < 1e-8)
			return true;
		else
			return false;
	}

	return false;
}


double ShapeAlgorithms::getDiagonal(const TopoDS_Shape& shape)
{
	Bnd_Box box;
	box.SetGap(0.000);
	BRepBndLib::Add(shape, box);
	Standard_Real xmin, ymin, zmin, xmax, ymax, zmax;
	box.Get(xmin, ymin, zmin, xmax, ymax, zmax);
	return sqrt(pow(xmax - xmin,2) + pow(ymax-ymin,2) + pow(zmax-zmin,2));
}


double ShapeAlgorithms::getMinEdgeLength(const TopoDS_Shape& shape)
{
	double min_edge_length = 1e20;
	for (TopExp_Explorer exp(shape, TopAbs_ShapeEnum::TopAbs_EDGE); exp.More(); exp.Next())
	{
		GProp_GProps gprops;
		BRepGProp::LinearProperties(exp.Current(), gprops);
		if (gprops.Mass() < min_edge_length)
			min_edge_length = gprops.Mass();
	}
	return min_edge_length;
}

double ShapeAlgorithms::getVolume(const TopoDS_Shape& shape)
{
	GProp_GProps gprops;
	BRepGProp::VolumeProperties(shape, gprops);
	return gprops.Mass();
}


double ShapeAlgorithms::getArea(const TopoDS_Shape& shape)
{
	GProp_GProps gprops;
	BRepGProp::SurfaceProperties(shape, gprops);
	return gprops.Mass();
}

double ShapeAlgorithms::getLength(const TopoDS_Shape& shape)
{
	GProp_GProps gprops;
	BRepGProp::LinearProperties(shape, gprops);
	return gprops.Mass();
}


double ShapeAlgorithms::getMinDistance(const TopoDS_Shape s1, const TopoDS_Shape s2, gp_Vec& vector)
{
	BRepExtrema_DistShapeShape mindist(s1, s2);
	double distance = 1e20;
	gp_Vec local_vector;
	if (mindist.IsDone())
	{
		for (int i = 1; i <= mindist.NbSolution(); i++)
		{
			gp_Pnt p1 = mindist.PointOnShape1(i);
			gp_Pnt p2 = mindist.PointOnShape2(i);
			double local_dist = p1.Distance(p2);
			if (distance > local_dist)
			{
				distance = local_dist;
				local_vector = gp_Vec(p1, p2);
			}
		}
	}

	vector = local_vector;
	return distance;
}


void ShapeAlgorithms::writeStepFile(const TopoDS_Shape& shape, const std::string filename)
{
	STEPControl_Writer writer;
	writer.Transfer(shape, STEPControl_AsIs);
	writer.Write(filename.c_str());
}

TopoDS_Shape ShapeAlgorithms::readStepFile(const std::string filename)
{

	TopoDS_Shape shape;
	STEPControl_Reader reader;
	reader.ReadFile(filename.c_str());

	// Turns out that opencascade scales the geometry automatically, so this is not needed
	//// adjust units
	//TColStd_SequenceOfAsciiString lengthUnits, angleUnits, solidAngleUnits;
	//reader.FileUnits(lengthUnits, angleUnits, solidAngleUnits);
	//if (lengthUnits.Length() > 0)
	//{
	//	Interface_Static::SetCVal("xstep.cascade.unit", stepToOCCUnits(lengthUnits.First().ToCString()).c_str());
	//}

	Interface_Static::SetIVal("read.step.nonmanifold", 1);

	reader.TransferRoots();
	shape = reader.OneShape();
	return shape;
}

TopoDS_Shape ShapeAlgorithms::readIgesFile(const std::string path)
{
	TopoDS_Shape shape;
	IGESControl_Reader reader;
	IFSelect_ReturnStatus status = reader.ReadFile(path.c_str());

	Handle(IGESData_IGESModel) model = reader.IGESModel();
	int nb_entities = model->NbEntities();

	// check if entities exist in iges model that belong to B-Rep types
	// otherwise, refuse to read!
	std::vector<int> brep_iges_types = { 186, 502, 504, 508, 510, 514 };
	bool found_brep_types = false;
	for (int i = 1; i <= nb_entities; i++)
	{
		Handle(IGESData_IGESEntity) ent = model->Entity(i);
		if (std::find(brep_iges_types.begin(), brep_iges_types.end(), ent->IGESType().Type()) != brep_iges_types.end())
		{
			found_brep_types = true;
			break;
		}
	}

	std::string fn = misc::getFileName(path);

	if (!found_brep_types)
		throw(cenos_exception("IGES file " + fn + " is not of B-Rep format. \n Please make sure your IGES file contains Manifold Solids! \n Usually in CAD editors there is an option to save IGES with Manifold Solids (B-Rep)"));



	switch (status)
	{
		case IFSelect_RetDone:
		{
			reader.ClearShapes();
			reader.TransferRoots();
			shape = reader.OneShape();
			break;
		}
		case IFSelect_RetVoid:
			throw(cenos_exception("No data to process in imported IGES file (" + fn + ")"));
		case IFSelect_RetError:
			throw(cenos_exception("Error in IGES file (" + fn + ") input data"));
		case IFSelect_RetFail:
			throw(cenos_exception("IGES file (" + fn + ") processing failed"));
		case IFSelect_RetStop:
			throw(cenos_exception("IGES file (" + fn + ") processing stopped with error"));
		default:
			throw(cenos_exception("Wrong format of the imported file (" + fn + "). Can't import file."));
	}
	return shape;
}

void ShapeAlgorithms::writeBrepFile(const TopoDS_Shape& shape, const std::string filename)
{
	
	BRepTools::Write(shape, filename.c_str(), true, true, TopTools_FormatVersion_VERSION_1);
}

gp_Pnt ShapeAlgorithms::getCenterPoint(const TopoDS_Shape& s1)
{
	if (getHighestShapeType(s1) == TopAbs_SOLID)
	{
		GProp_GProps solidProps;
		BRepGProp::VolumeProperties(s1, solidProps);
		return solidProps.CentreOfMass();
	}
	else if (getHighestShapeType(s1) == TopAbs_FACE)
	{
		GProp_GProps faceProps;
		BRepGProp::SurfaceProperties(s1, faceProps);
		return faceProps.CentreOfMass();
	}
	else if (getHighestShapeType(s1) == TopAbs_EDGE)
	{
		GProp_GProps edgeProps;
		BRepGProp::LinearProperties(s1, edgeProps);
		return edgeProps.CentreOfMass();
	}
}



bool ShapeAlgorithms::hasInternalFaces(const TopoDS_Shape& s1)
{
	TopTools_IndexedMapOfShape shellmap;
	TopTools_IndexedMapOfShape fmap;
	TopExp::MapShapes(s1, TopAbs_SHELL, shellmap);
	for (int sh = 1; sh <= shellmap.Extent(); sh++)
	{
		TopExp::MapShapes(shellmap.FindKey(sh), TopAbs_FACE, fmap);
		for (int f = 1; f <= fmap.Extent(); f++)
		{
			if (fmap.FindKey(f).Orientation() == TopAbs_Orientation::TopAbs_INTERNAL)
			{
				return true;
			}
		}
	}

	return false;
}



TopTools_ListOfShape ShapeAlgorithms::sortEdges(const TopoDS_Shape& edge_shape)
{
	double tolerance = 1e-8;

	std::vector<TopoDS_Edge> edges;

	for (TopExp_Explorer explorer(edge_shape, TopAbs_ShapeEnum::TopAbs_EDGE); explorer.More(); explorer.Next())
	{
		TopoDS_Edge e = TopoDS::Edge(explorer.Current());
		edges.push_back(e);
	}

	TopTools_ListOfShape wire_list; //resulting list of wires that will be returned

	TopoDS_Edge current_edge = edges[0];
	// make both points same
	TopoDS_Vertex vstart = TopExp::FirstVertex(current_edge);
	TopoDS_Vertex vend = TopExp::FirstVertex(current_edge);

	//loop through all edges and add them into connected wires
	while (!edges.empty())
	{
		BRepBuilderAPI_MakeWire wire;
		
		auto it = edges.begin();
		while (it != edges.end())
		{
			TopoDS_Vertex v1 = TopExp::FirstVertex(*it);
			TopoDS_Vertex v2 = TopExp::LastVertex(*it);

			if (compare_vertices(vstart, v1))
			{
				vstart = v2;
				current_edge = *it;
				wire.Add(*it);
				it = edges.erase(it);
			}
			else if (compare_vertices(vstart, v2))
			{
				vstart = v1;
				current_edge = *it;
				wire.Add(*it);
				it = edges.erase(it);
			}
			else if (compare_vertices(vend, v1))
			{
				vend = v2;
				current_edge = *it;
				wire.Add(*it);
				it = edges.erase(it);
			}
			else if (compare_vertices(vend, v2))
			{
				vend = v1;
				current_edge = *it;
				wire.Add(*it);
				it = edges.erase(it);
			}
			else 
			{
				++it;
			}
		}

		wire_list.Append(wire.Wire());

		if (!edges.empty())
		{
			current_edge = edges[0];
			vstart = TopExp::FirstVertex(current_edge);
			vend = TopExp::FirstVertex(current_edge);
		}
	}

	return wire_list;
}


gp_Dir ShapeAlgorithms::getFaceNormal(TopoDS_Shape shape)
{
	if (getHighestShapeType(shape) != TopAbs_ShapeEnum::TopAbs_FACE)
		throw(cenos_exception("Face normal requested for shape that is not face"));
	gp_Dir dir;

	BOPTools_AlgoTools3D::GetNormalToSurface(BRep_Tool::Surface(TopoDS::Face(shape)), 0, 0, dir);

	return dir;
}

std::string ShapeAlgorithms::stepToOCCUnits(std::string stepUnit)
{
	std::transform(stepUnit.begin(), stepUnit.end(), stepUnit.begin(),
		[](unsigned char c) { return std::tolower(c); });
	// map for Step units to occ units
	std::map<std::string, std::string> to_occ_units = {
		{"inch", "INCH"},
		{"mil", "MIL"},
		{"microinch", "UIN"},
		{"foot", "FT"},
		{"mile", "MI"},
		{"metre", "M"},
		{"kilometre", "KM"},
		{"millimetre", "MM"},
		{"centimetre", "CM"},
		{"micrometre", "UM"},
		{"", "M"}
	};

	if (to_occ_units.find(stepUnit) != to_occ_units.end())
	{
		return to_occ_units[stepUnit];
	}
	else
	{
		std::cerr << "Could not determine unit " << stepUnit << " in step file. Using meters instead" << std::endl;
		return "M";
	}
}


TopoDS_Shape ShapeAlgorithms::scaleShape(const TopoDS_Shape shape, double factor)
{
	if (shape.IsNull())
		throw(cenos_exception("Empty shape passed to scaleShape()"));

	gp_Trsf scale_transform;
	scale_transform.SetScale(gp_Pnt(0,0,0), factor);
	return BRepBuilderAPI_Transform(shape, scale_transform).Shape();
}