#include "TemplateBuilder.h"
#include "PatchAntennaTemplate.h"
#include "CircularPatchAntennaTemplate.h"
#include "TriangleMicrostripAntennaTemplate.h"
#include "YaguiUdaAntennaTemplate.h"
#include "DipoleAntennaTemplate.h"
#include "AperturePatchAntennaTemplate.h"
#include "WorkpieceTemplate.h"
#include "InductorTemplate.h"
#include "EOFBlockTemplate.h"
#include "EOFMagnetTemplate.h"
#include "topology/ShapeAlgorithms.h"
#include <Precision.hxx>
#include "cenos_exception.h"

TemplateBuilder::TemplateBuilder(std::shared_ptr<Job> job): stopper(nullptr)
{
	project_path = job->getArg("caseDir").value();
	//temporary fix - currently different structure in generateTemplateGeometry and generateTemplateMesh
	if (job->command() == "generateTemplateGeometry")
		params = job->getArg("templateParams").value();
	if (job->command() == "generateTemplateMesh")
	{
		params = job->getArg("templateParams").value();
		std::string mesh_density;
		mesh_density  = job->getArg("meshParams").value()["meshDensity"].get<std::string>();
		// Mesh density parameters are empyrical! Obtained by trial and error
		if (mesh_density == "rough")
			mesh_density_factor = 1.2;
		else if (mesh_density == "fine")
			mesh_density_factor = 0.7;
		else
			mesh_density_factor = 1.0;
	}

	if (params.find("geometryUnit") != params.end())
		unit = stringToLengthUnit(params["geometryUnit"].get<std::string>());
	else
		unit = LengthUnit::METER;


	// check if start frequency is smaller than end frequency for multiharmonic times
	if (job->getArg("physicsParams").has_value())
	{
		json phys_json = job->getArg("physicsParams").value();

		for (auto ph : phys_json["physicsList"])
		{
			Physics phys;
			phys.readInput(ph);
			phys.validateInput();
			physics.push_back(phys);
		}
	}
}

void TemplateBuilder::buildTemplate()
{
	json templates_json = params["templates"];

	for (const auto& temp : templates_json.items())
	{
		json temp_value = temp.value();
		templateType type = getTemplateType(temp_value);

		BasicTemplate_ templ = nullptr;
		switch (type) {
		case PATCH_ANTENNA:
			templ = std::make_shared<PatchAntennaTemplate>(project_path);
			break;
		case CIRCULAR_PATCH_ANTENNA:
			templ = std::make_shared<CircularPatchAntennaTemplate>(project_path);
			break;
		case TRIANGLE_MICROSTRIP_ANTENNA:
			templ = std::make_shared<TriangleMicrostripAntennaTemplate>(project_path);
			break;
		case YAGUI_UDA_ANTENNA:
			templ = std::make_shared<YaguiUdaAntennaTemplate>(project_path);
			break;
		case DIPOLE_ANTENNA:
			templ = std::make_shared <DipoleAntennaTemplate>(project_path);
			break;
		case APERTURE_PATCH_ANTENNA:
			templ = std::make_shared < AperturePatchAntennaTemplate>(project_path);
			break;
		case WORKPIECE:
			templ = std::make_shared<WorkpieceTemplate>(project_path);
			break;
		case INDUCTOR:
			templ = std::make_shared<InductorTemplate>(project_path);
			break;
		case BLOCK:
			templ = std::make_shared<EOFBlockTemplate>(project_path);
			break;
		case MAGNET:
			templ = std::make_shared<EOFMagnetTemplate>(project_path);
			break;
		}

		if (templ != nullptr)
		{   
			templ->parseInputJSON(&temp_value);
			templ->setPhysics(physics);
			templates.push_back(templ);
		}
	}

}

bool TemplateBuilder::validate(std::string & msg)
{
	for (auto templ : templates)
	{
		if (!templ->validate(msg))
			return false;
	}
	return true;
}

void TemplateBuilder::generateMesh()
{
	MeshGenerator_ mg = std::make_shared<MeshGenerator>(geom_data);
	MeshParameters_ mp_global = std::make_shared<MeshParameters>();

	auto sizes = calculateGlobalMeshSizing();
	double global_min = sizes.first;
	double global_max = sizes.second;

	mp_global->setMaxH(global_max * mesh_density_factor);
	mp_global->setMinH(global_min * mesh_density_factor);
	mg->setParameters(mp_global);

	for (auto templ : templates)
	{
		templ->assignMeshSizes(mg, geom_data, mesh_density_factor, global_max);
	}

	if (progress != nullptr)
		mg->setProgressIndicator(progress);
	mg->setStopper(stopper);
	std::shared_ptr<Mesh> mesh = mg->generateMesh();

	mesh->writeMesh(project_path + "\\geometry\\meshFile.msh", std::string("GMSH"));

	geom_data->setMeshFile(project_path + "\\geometry\\meshFile.msh");
	geom_data->setMeshStats(mesh->getMeshStats());
}

templateType TemplateBuilder::getTemplateType(json templ_json)
{
	if (std::string(templ_json["templateShape"]).compare("antenna") == 0)
	{
		templateType type = templateType::PATCH_ANTENNA;

		if (std::string(templ_json["shapeType"]).compare("dipoleAntenna") == 0)
		{
			type = templateType::DIPOLE_ANTENNA;
		}
		else if (std::string(templ_json["shapeType"]).compare("patchApertureFedAntenna") == 0)
		{
			type = templateType::APERTURE_PATCH_ANTENNA;
		}
		else if (std::string(templ_json["shapeType"]).compare("CircularPatchAntennaTemplate") == 0)
		{
			type = templateType::CIRCULAR_PATCH_ANTENNA;
		}
		else if (std::string(templ_json["shapeType"]).compare("triangleMicrostripAntennaTemplate") == 0)
		{
			type = templateType::TRIANGLE_MICROSTRIP_ANTENNA;
		}
		else if (std::string(templ_json["shapeType"]).compare("yaguiUdaAntenna") == 0)
		{
			type = templateType::YAGUI_UDA_ANTENNA;
		}
		return type;
	}
	else if (std::string(templ_json["templateShape"]).compare("workpiece") == 0)
	{
		return templateType::WORKPIECE;
	}
	else if (std::string(templ_json["templateShape"]).compare("inductor") == 0)
	{
		return templateType::INDUCTOR;
	}
	else if (std::string(templ_json["templateShape"]).compare("melt") == 0)
	{
		return templateType::BLOCK;
	}
	else if (std::string(templ_json["templateShape"]).compare("magnet") == 0)
	{
		return templateType::MAGNET;
	}

}

double TemplateBuilder::calculatePadding()
{
	double padding = 0;
	for (auto phys : physics)
	{
		// for microwaves - padding should be wavelength / 2
		if (phys.getSolverKey() == "getdpMicrowaves")
		{
			double frequency = phys.getTimeParameters()["frequency"].getDoubleValue() * phys.getTimeParameters()["frequency"].getFactor();
			double wavelength = 299792458.0 / frequency / lengthToSI(unit);
			padding = wavelength / 2;
		}
		else if (phys.getSolverKey() == "getdpEMGeneral2D")
		{
			// currently use inductor outer diameter for infinity padding. What to use for 3D?
			ShapeStats stat = geom_data->getShapeStatistics();
			padding = stat.maxX * 3;
		}
		else if (phys.getSolverKey() == "elmerRotatingMagnets")
		{
			ShapeStats stat = geom_data->getShapeStatistics();
			padding = ShapeAlgorithms::getDiagonal(geom_data->getShape());
		}

	}

	//if no physics, calculate sizing from template type
	if (physics.empty())
	{
		for (auto templ : templates)
		{
			if (templ->getType() == templateType::APERTURE_PATCH_ANTENNA or
				templ->getType() == templateType::PATCH_ANTENNA or
				templ->getType() == templateType::DIPOLE_ANTENNA or
				templ->getType() == templateType::CIRCULAR_PATCH_ANTENNA or
				templ->getType() == templateType::TRIANGLE_MICROSTRIP_ANTENNA or
				templ->getType() == templateType::YAGUI_UDA_ANTENNA
				)
			{
				double wavelength = 299792458.0 / 2.4e9 / lengthToSI(unit);
				padding = wavelength / 2;
			}
			else if (templ->getType() == templateType::INDUCTOR)
			{
				ShapeStats stat = geom_data->getShapeStatistics();
				padding = stat.maxX * 3;
			}
		}
	}

	return padding;
}


std::pair<double, double> TemplateBuilder::calculateGlobalMeshSizing()
{
	double global_max = 0, global_min = 0;
	// calculate physics-related mesh parameters
	for (auto phys : physics)
	{
		// for microwaves - mesh size should not be smaller than wavelength / 10 ( or /5 for quadratic interpolation fns???)
		if (phys.getSolverKey() == "getdpMicrowaves")
		{
			double frequency = phys.getTimeParameters()["frequency"].getDoubleValue() * phys.getTimeParameters()["frequency"].getFactor();
			double wavelength = 299792458.0 / frequency / lengthToSI(unit);
			global_max = wavelength / 5;
		}
		else if (phys.getSolverKey() == "getdpEMGeneral2D" or phys.getSolverKey() == "getdpEMGeneral3D")
		{
			ShapeStats stat = geom_data->getShapeStatistics();

			//this works for 2D only!
			global_max = std::min(stat.maxY - stat.minY, stat.maxX * 3) / 15;
		}
		else if (phys.getSolverKey() == "elmerRotatingMagnets")
		{
			ShapeStats stat = geom_data->getShapeStatistics();

			global_max = sqrt(std::pow(stat.maxX - stat.minX, 2) + std::pow(stat.maxY - stat.minY, 2) + std::pow(stat.maxZ - stat.minZ, 2)) / 10;
		}
	}

	if (global_max == 0)
		throw(cenos_exception("Error during assigning template mesh size. Max size cannot be 0. This is internal error. Please contact support!"));

	return std::pair<double, double>(global_min, global_max);
}

GeometryData_ TemplateBuilder::getGeometryData()
{
	return geom_data;
}

GeometryData_ TemplateBuilder::getMeshingData()
{
	return geom_data;
}

void TemplateBuilder::setProgressIndicator(std::shared_ptr<ProgressIndicator> pi)
{
	progress = pi;
}

void TemplateBuilder::generateGeometry()
{

	if (templates.empty())
		throw(cenos_exception("Error during template generation. Template vector is empty. This is internal error. Please report to the support!"));


	// ******************************************
	// check if templates of equal type are present, and increase counter to prevent name clash
	std::map<templateType, std::vector<BasicTemplate_>> templates_by_type;
	for (auto templ : templates)
		templates_by_type[templ->getType()].push_back(templ);
	//assign index
	for (auto templ_type : templates_by_type)
	{
		if (templ_type.second.size() > 1)
		{
			int index = 0;
			for (auto templ : templ_type.second)
			{
				index++;
				templ->setIndex(index);
			}
		}
	}
	// ******************************************


	for (auto templ : templates)
	{
		templ->generateGeometry();
	}

	//validate if templates overlap
	for (int i = 0; i < templates.size(); i++)
	{
		for (int j = 0; j < templates.size(); j++)
		{
			if (i == j)
				continue;

			TopoDS_Shape s_i = templates[i]->getGeometryData()->getShape();
			TopoDS_Shape s_j = templates[j]->getGeometryData()->getShape();
			if (ShapeAlgorithms::getMinDistance(s_i, s_j) <= Precision::Confusion())
			{
				std::string msg = "Objects " + templates[i]->getName() + " and " + templates[j]->getName() + " are overlapping or touching. Please check input data!";
				throw(cenos_exception(msg));
			}
		}
	}

	//Construct geometryData from templates
	geom_data = std::make_shared<GeometryData>(project_path);
	for (auto templ : templates)
	{

		for (auto ent : templ->getGeometryData()->getEntities())
			geom_data->addEntity(ent);

		for (auto bnd : templ->getGeometryData()->getBoundaries())
			geom_data->addBoundaryGroup(bnd);

		for (auto dom : templ->getGeometryData()->getDomains())
			geom_data->addDomainGroup(dom);
	}

	if (templates[0]->getType() == WORKPIECE or templates[0]->getType() == INDUCTOR)
		geom_data->setAxisymmetric(true);
	else
		geom_data->setAxisymmetric(false);


	geom_data->setLengthUnit(unit);
	geom_data->setGroupMode(GeometryData::Mode::GROUPS_FROM_PREPROCESSOR);

	geom_data->addAir( calculatePadding() );
	geom_data->update();
}

void TemplateBuilder::setStopper(bool* b)
{
	stopper = b;
}