#include "InductorTemplate.h"

#include <GC_MakeSegment.hxx>
#include <BRepBuilderAPI_MakeEdge.hxx>
#include <BRepBuilderAPI_MakeWire.hxx>
#include <BRepBuilderAPI_MakeFace.hxx>
#include <BRepPrimAPI_MakeBox.hxx>
#include <TopoDS.hxx>
#include <TopoDS_Wire.hxx>
#include <TopoDS_Shell.hxx>
#include <TopoDS_Solid.hxx>
#include <TopoDS_Vertex.hxx>
#include <TopoDS_Face.hxx>
#include <TopTools_MapOfShape.hxx>
#include <BRepAlgoAPI_BuilderAlgo.hxx>
#include <BRepAlgoAPI_Cut.hxx>
#include <STEPControl_Writer.hxx>
#include <TopExp_Explorer.hxx>
#include <GProp_GProps.hxx>
#include <gp_Circ.hxx>
#include <Geom_Circle.hxx>
#include <BRepGProp.hxx>
#include <BRepPrimAPI_MakeSphere.hxx>
#include <BRepPrimAPI_MakeCylinder.hxx>
#include <BRepBndLib.hxx>
#include <map>
#include <filesystem>
#include "easylogging++/easylogging++.h"
#include <TopTools_IndexedMapOfShape.hxx>
#include "topology/ShapeAlgorithms.h"
#include "vtkForCenos/topologyToVTK.h"
#include "misc/miscFunctions.hpp"
#include "mesh/MeshGenerator.h"
#include "mesh/AutoMeshSize.h"
#include "topology/shapePartition.h"

InductorTemplate::InductorTemplate(std::string path) : BasicTemplate(path)
{
    type = INDUCTOR;
    additional_index = "";
}

void InductorTemplate::parseInputJSON(json* j)
{
    json& data = *j;
    name = data["name"].get<std::string>();
    if (data["shapeType"].get<std::string>() == "inductorCircularProfile")
    {
        ind_type = CIRCULAR;
        coil_diameter = data["properties"]["coil_diameter"].get<double>();
        coil_winding_count = data["properties"]["coil_winding_count"]["value"].get<int>();
        coil_height = data["properties"]["coil_height"].get<double>();
        coil_profile_diameter = data["properties"]["coil_profile_diameter"].get<double>();
        coil_center_offset = data["properties"]["coil_center_offset"].get<double>();
        if (data["properties"].find("coil_profile_thickness") != data["properties"].end())
            coil_profile_thickness = data["properties"]["coil_profile_thickness"].get<double>();
        else
            coil_profile_thickness = 0;
    }
    else if (data["shapeType"].get<std::string>() == "inductorRectangularProfile")
    {
        ind_type = RECTANGULAR;
        coil_diameter = data["properties"]["coil_diameter"].get<double>();
        coil_winding_count = data["properties"]["coil_winding_count"]["value"].get<int>();
        coil_height = data["properties"]["coil_height"].get<double>();
        coil_profile_height = data["properties"]["coil_profile_height"].get<double>();
        coil_profile_width = data["properties"]["coil_profile_width"].get<double>();
        coil_center_offset = data["properties"]["coil_center_offset"].get<double>();
        if (data["properties"].find("coil_profile_thickness") != data["properties"].end())
            coil_profile_thickness = data["properties"]["coil_profile_thickness"].get<double>();
        else
            coil_profile_thickness = 0;
    }
    else if (data["shapeType"].get<std::string>() == "inductorWithConcentrator")
    {
        ind_type = RECTANGULAR_WITH_CONC;
        coil_diameter = data["properties"]["coil_diameter"].get<double>();
        coil_winding_count = data["properties"]["coil_winding_count"]["value"].get<int>();
        coil_height = data["properties"]["coil_height"].get<double>();
        coil_profile_height = data["properties"]["coil_profile_height"].get<double>();
        coil_profile_width = data["properties"]["coil_profile_width"].get<double>();
        coil_center_offset = data["properties"]["coil_center_offset"].get<double>();
        if (data["properties"].find("coil_profile_thickness") != data["properties"].end())
            coil_profile_thickness = data["properties"]["coil_profile_thickness"].get<double>();
        else
            coil_profile_thickness = 0;
        concentrator_width1 = data["properties"]["concentrator_width1"].get<double>();
        concentrator_width2 = data["properties"]["concentrator_width2"].get<double>();  
    }

}

void InductorTemplate::makeCircularCoil()
{
    double coil_distance, coil_bottom_coordinate;
    if (coil_winding_count != 1)
    {
        coil_distance = (coil_height - coil_profile_diameter) / (coil_winding_count - 1.0);
        coil_bottom_coordinate = -coil_height / 2.0 + coil_center_offset;
    }
    else
    {
        coil_distance = 0;
        coil_bottom_coordinate = coil_center_offset - coil_profile_diameter / 2.0;
    }

    for (int i = 0; i < coil_winding_count; i++)
    {
        gp_Dir zDir(0, 0, 1);

        double rBaseCircle = 50.;

        gp_Pnt base_pnt(coil_diameter / 2.0 - coil_profile_diameter / 2.0, coil_profile_diameter / 2.0 + coil_bottom_coordinate + coil_distance * i, 0);
        TopoDS_Shape winding;
        if (coil_profile_thickness == 0)
        {
            gp_Circ circle(gp_Ax2(base_pnt, zDir), coil_profile_diameter / 2.0);
            TopoDS_Edge edge = BRepBuilderAPI_MakeEdge(circle);
            TopoDS_Wire wire = BRepBuilderAPI_MakeWire(edge);
            winding = BRepBuilderAPI_MakeFace(wire, Standard_True);
        }
        else
        {
            gp_Circ circle_out(gp_Ax2(base_pnt, zDir), coil_profile_diameter / 2.0);
            gp_Circ circle_in(gp_Ax2(base_pnt, -zDir), coil_profile_diameter / 2.0 - coil_profile_thickness);
            TopoDS_Edge edge_out = BRepBuilderAPI_MakeEdge(circle_out);
            TopoDS_Edge edge_in = BRepBuilderAPI_MakeEdge(circle_in);
            TopoDS_Wire wire_out = BRepBuilderAPI_MakeWire(edge_out);
            TopoDS_Wire wire_in = BRepBuilderAPI_MakeWire(edge_in);
            BRepBuilderAPI_MakeFace face(wire_out, Standard_True);
            face.Add(wire_in);
            winding = face.Shape();
        }

        TopoEntity_ winding_ent = std::make_shared<TopoEntity>(winding, "winding_ent_" + std::to_string(i) + additional_index);
        geom_data->addEntity(winding_ent);
        DomainGroup_ winding_group = std::make_shared<DomainGroup>();
        winding_group->setName("winding_"+std::to_string(i) + additional_index);
        winding_group->setLabel("Winding_" + std::to_string(i) + additional_index);
        winding_group->setRole("inductor");
        winding_group->setInGroup("Coil" + additional_index);
        winding_group->addEntity(winding_ent);
        winding_group->update();
        geom_data->addDomainGroup(winding_group);

    }
}


void InductorTemplate::makeRectangularCoil()
{
    double coil_distance, coil_bottom_coordinate;
    if (coil_winding_count != 1)
    {
        coil_distance = (coil_height - coil_profile_height) / (coil_winding_count - 1.0);
        coil_bottom_coordinate = -coil_height / 2.0 + coil_center_offset;
    }
    else
    {
        coil_distance = 0;
        coil_bottom_coordinate = coil_center_offset - coil_profile_height / 2.0;
    }

    for (int i = 0; i < coil_winding_count; i++)
    {
        gp_Pnt pnt[4] = { gp_Pnt(coil_diameter / 2.0 - coil_profile_width, coil_bottom_coordinate + coil_distance * i, 0),
                            gp_Pnt(coil_diameter / 2.0, coil_bottom_coordinate + coil_distance * i, 0),
                            gp_Pnt(coil_diameter / 2.0, coil_profile_height + coil_bottom_coordinate + coil_distance * i, 0),
                            gp_Pnt(coil_diameter / 2.0 - coil_profile_width, coil_profile_height + coil_bottom_coordinate + coil_distance * i, 0) };
        BRepBuilderAPI_MakeWire f_wire;
        for (int j = 0; j < 4; j++)
        {
            Handle(Geom_TrimmedCurve) f_segment = GC_MakeSegment(pnt[j], pnt[j == 3 ? 0 : j + 1]);
            TopoDS_Edge f_edge = BRepBuilderAPI_MakeEdge(f_segment);
            f_wire.Add(f_edge);
        }

        TopoDS_Shape winding;

        BRepBuilderAPI_MakeFace mk_face(f_wire.Wire(), Standard_True);
        if (coil_profile_thickness == 0)
        {
        }
        else
        {
            gp_Pnt pnt_in[4] = { gp_Pnt(coil_diameter / 2.0 - coil_profile_width + coil_profile_thickness, coil_bottom_coordinate + coil_distance * i + coil_profile_thickness, 0),
                                    gp_Pnt(coil_diameter / 2.0 - coil_profile_width + coil_profile_thickness, coil_profile_height + coil_bottom_coordinate + coil_distance * i - coil_profile_thickness, 0),
                                    gp_Pnt(coil_diameter / 2.0 - coil_profile_thickness, coil_profile_height + coil_bottom_coordinate + coil_distance * i - coil_profile_thickness, 0),
                                    gp_Pnt(coil_diameter / 2.0 - coil_profile_thickness, coil_bottom_coordinate + coil_distance * i + coil_profile_thickness, 0) };
            BRepBuilderAPI_MakeWire fin_wire;
            for (int j = 0; j < 4; j++)
            {
                Handle(Geom_TrimmedCurve) fin_segment = GC_MakeSegment(pnt_in[j], pnt_in[j == 3 ? 0 : j + 1]);
                TopoDS_Edge fin_edge = BRepBuilderAPI_MakeEdge(fin_segment);
                fin_wire.Add(fin_edge);
            }
            mk_face.Add(fin_wire);
        }

        winding = mk_face.Shape();

        TopoEntity_ winding_ent = std::make_shared<TopoEntity>(winding, "winding_ent_" + std::to_string(i) + additional_index);
        geom_data->addEntity(winding_ent);
        DomainGroup_ winding_group = std::make_shared<DomainGroup>();
        winding_group->setName("winding_" + std::to_string(i) + additional_index);
        winding_group->setLabel("Winding_" + std::to_string(i) + additional_index);
        winding_group->setRole("inductor");
        winding_group->setInGroup("Coil" + additional_index);
        winding_group->addEntity(winding_ent);
        winding_group->update();
        geom_data->addDomainGroup(winding_group);
    }
}

void InductorTemplate::makeConcentrator()
{
    double coil_bottom_coordinate, coil_top_coordinate;
    if (coil_winding_count != 1)
    {
        coil_bottom_coordinate = -coil_height / 2.0 + coil_center_offset;
        coil_top_coordinate = coil_bottom_coordinate + coil_height;
    }
    else
    {
        coil_bottom_coordinate = coil_center_offset - coil_profile_height / 2.0;
        coil_top_coordinate = coil_bottom_coordinate + coil_profile_height;
    }

    gp_Pnt pnt[8] = { gp_Pnt(coil_diameter / 2.0, coil_bottom_coordinate, 0),
          gp_Pnt(coil_diameter / 2.0, coil_top_coordinate, 0),
          gp_Pnt(coil_diameter / 2.0 - coil_profile_width, coil_top_coordinate, 0),
          gp_Pnt(coil_diameter / 2.0 - coil_profile_width, coil_top_coordinate + concentrator_width1, 0),
          gp_Pnt(coil_diameter / 2.0 + concentrator_width2, coil_top_coordinate + concentrator_width1, 0),
          gp_Pnt(coil_diameter / 2.0 + concentrator_width2, coil_bottom_coordinate - concentrator_width1, 0),
          gp_Pnt(coil_diameter / 2.0 - coil_profile_width, coil_bottom_coordinate - concentrator_width1, 0),
          gp_Pnt(coil_diameter / 2.0 - coil_profile_width, coil_bottom_coordinate, 0)};

    BRepBuilderAPI_MakeWire f_wire;
    for (int j = 0; j < 8; j++)
    {
        Handle(Geom_TrimmedCurve) f_segment = GC_MakeSegment(pnt[j], pnt[j == 7 ? 0 : j + 1]);
        TopoDS_Edge f_edge = BRepBuilderAPI_MakeEdge(f_segment);
        f_wire.Add(f_edge);
    }
    BRepBuilderAPI_MakeFace mk_face(f_wire.Wire(), Standard_True);

    //Shape needs to be reversed as points for creation are in reversed order
    TopoDS_Shape concentrator = mk_face.Shape().Reversed();


    TopoEntity_ concentrator_ent = std::make_shared<TopoEntity>(concentrator, "concentrator_ent" + additional_index);
    geom_data->addEntity(concentrator_ent);
    DomainGroup_ concentrator_group = std::make_shared<DomainGroup>();
    concentrator_group->setName("concentrator" + additional_index);
    concentrator_group->setLabel("Concentrator" + additional_index);
    concentrator_group->setRole("concentrator");
    concentrator_group->addEntity(concentrator_ent);
    concentrator_group->update();
    geom_data->addDomainGroup(concentrator_group); 
}


void InductorTemplate::generateGeometry()
{
    if (progress != nullptr)
        progress->sendLog("Generating inductor template geometry ");

    if (index > 0)
        additional_index = "_" + std::to_string(index);
    TopoDS_Shape inductor;
    TopoDS_Shape concentrator;
    //shapes for boundaries

    if (ind_type == CIRCULAR)
    {
        makeCircularCoil();
    }
    else if (ind_type == RECTANGULAR)
    {
        makeRectangularCoil();
    }
    else if (ind_type == RECTANGULAR_WITH_CONC)
    {
        makeRectangularCoil();
        makeConcentrator();
    }
}

void InductorTemplate::assignMeshSizes(MeshGenerator_ mg, GeometryData_ gd, double mesh_density_factor, double global_max)
{
    // for electromagnetics - calculate skin depth range for each conducting domain
    double skin_min = 0, skin_max = 0;
    bool assign_layers = false;
    for (auto phys : physics)
    {
        if (phys.getSolverKey() == "getdpEMGeneral2D" or phys.getSolverKey() == "getdpEMGeneral3D")
        {
            float frequency_min = phys.getTimeParameters()["frequency"].getMinValue();
            float frequency_max = phys.getTimeParameters()["frequency"].getMaxValue();

            for (auto dom : phys.getDomainsList())
            {
                if (dom->getName() != "workpiece")
                    continue;

                if (dom->getTypeKey() == "em_cc" or dom->getTypeKey() == "source_dom")
                {
                    auto mat = phys.getMaterialById(dom->getMaterialId());
                    auto skin_layers = mat.getSkinLayerThicknessRange(frequency_min, frequency_max);
                    skin_min = skin_layers.first / lengthToSI(gd->getLengthUnit());
                    skin_max = skin_layers.second / lengthToSI(gd->getLengthUnit());
                    assign_layers = true;
                }
            }
        }
    }

    std::shared_ptr<MeshParameters> mp_ind = std::make_shared<MeshParameters>();
    double maxh_inductor = 0, minh_inductor = 0;
    double maxh_concentrator = 0, minh_concentrator = 0;
    if (ind_type == CIRCULAR)
    {
        if (coil_profile_thickness != 0)
        {
            maxh_inductor = std::min({ global_max, coil_profile_thickness / 3.0 });
            minh_inductor = coil_profile_thickness / 6.0;
            if (assign_layers)
                mp_ind->calculateLayers(skin_min, skin_max, coil_profile_thickness / 4.0);
        }
        else
        {
            maxh_inductor = std::min({ global_max, coil_profile_diameter / 5.0 });
            minh_inductor = coil_profile_diameter / 15.0;
            if (assign_layers)
                mp_ind->calculateLayers(skin_min, skin_max, coil_profile_diameter / 5.0);
        }
    }
    else if (ind_type == RECTANGULAR)
    {
        if (coil_profile_thickness != 0)
        {
            maxh_inductor = std::min({ global_max, coil_profile_thickness / 3.0 });
            minh_inductor = coil_profile_thickness / 6.0;
            if (assign_layers)
                mp_ind->calculateLayers(skin_min, skin_max, coil_profile_thickness / 3.0);
        }
        else
        {
            maxh_inductor = std::min({ global_max, coil_profile_width / 5.0, coil_profile_height / 5.0 });
            minh_inductor = std::min({ coil_profile_width / 15.0, coil_profile_height / 15.0 });
            if (assign_layers)
                mp_ind->calculateLayers(skin_min, skin_max, std::min(coil_profile_height, coil_profile_thickness) / 5.0);
        }
    }
    else if (ind_type == RECTANGULAR_WITH_CONC)
    {
        if (coil_profile_thickness != 0)
        {
            maxh_inductor = std::min({ global_max, coil_profile_thickness / 3.0 });
            minh_inductor = coil_profile_thickness / 6.0;
            if (assign_layers)
                mp_ind->calculateLayers(skin_min, skin_max, coil_profile_thickness / 3.0);
        }
        else
        {
            maxh_inductor = std::min({ global_max, coil_profile_width / 5.0, coil_profile_height / 5.0 });
            minh_inductor = std::min({ coil_profile_width / 15.0, coil_profile_height / 15.0 });
            if (assign_layers)
                mp_ind->calculateLayers(skin_min, skin_max, std::min(coil_profile_height, coil_profile_thickness) / 5.0);
        }

        maxh_concentrator = std::min(concentrator_width1, concentrator_width2) / 3.0;
        minh_concentrator = std::min(concentrator_width1, concentrator_width2) / 10.0;
    }


    // concentrator
    if (maxh_concentrator != 0)
    {
        std::shared_ptr<MeshParameters> mp_conc = std::make_shared<MeshParameters>();
        if (minh_concentrator * 1.5 > maxh_concentrator)
            minh_concentrator = minh_concentrator / 2;

        mp_conc->setMaxH(maxh_concentrator * mesh_density_factor);
        mp_conc->setMinH(minh_concentrator * mesh_density_factor);

        auto conc_group = gd->getDomainByName("concentrator" + additional_index);

        for (auto ent : conc_group->getEntities())
        {
            mg->setParameters(mp_conc, ent);
            for (auto subent : gd->getEntities())
            {
                if (subent->isSubShapeOf(ent))
                    mg->setParameters(mp_conc, subent);
            }
        }
    }

    // inductor

    if (minh_inductor * 1.5 > maxh_inductor)
        minh_inductor = minh_inductor / 2;

    mp_ind->setMaxH(maxh_inductor * mesh_density_factor);
    mp_ind->setMinH(minh_inductor * mesh_density_factor);
    for (int i = 0; i < coil_winding_count; i++)
    {
        auto wp_group = gd->getDomainByName("winding_" + std::to_string(i) + additional_index);

        for (auto ent : wp_group->getEntities())
        {
            mg->setParameters(mp_ind, ent);
            for (auto subent : gd->getEntities())
            {
                if (subent->isSubShapeOf(ent))
                    mg->setParameters(mp_ind, subent);
            }
        }
    }

}

bool InductorTemplate::validate(std::string& msg)
{

    if (coil_diameter <= 0)
    {
        msg = "Coil diameter should be positive number.";
        return false;
    }

    if (ind_type == CIRCULAR)
    {
        if (coil_profile_thickness > coil_diameter / 2.0)
        {
            msg = "Coil profile diameter should be greater than double coil profile thickness.";
            return false;
        }

        if (coil_profile_diameter >= coil_diameter)
        {
            msg = "Coil diameter should be greater than coil profile diameter.";
            return false;
        }
        if (coil_profile_diameter * coil_winding_count >= coil_height and coil_winding_count > 1)
        {
            msg = "Coil height is too small for input number of windings.";
            return false;
        }
    }
    else if ( (ind_type == RECTANGULAR ) or (ind_type = RECTANGULAR_WITH_CONC))
    {
        if (coil_profile_height > coil_diameter / 2.0)
        {
            msg = "Coil profile height should be greater than double coil profile thickness.";
            return false;
        }

        if (coil_profile_width > coil_diameter / 2.0)
        {
            msg = "Coil profile width should be greater than double coil profile thickness.";
            return false;
        }

        if (coil_profile_height <= 0 or coil_profile_width <= 0)
        {
            msg = "Coil profile dimensions should be positive number.";
            return false;
        }

        if (coil_profile_height * coil_winding_count >= coil_height and coil_winding_count > 1)
        {
            msg = "Coil height is too small for input number of windings.";
            return false;
        }

        if (coil_profile_width >= coil_diameter)
        {
            msg = "Coil diameter should be greater than coil profile width.";
            return false;
        }
    }
    else if (ind_type == RECTANGULAR_WITH_CONC)
    {
        if (concentrator_width1 <= 0 or concentrator_width2 <= 0)
        {
            msg = "Concentrator dimensions should be positive number.";
            return false;
        }
    }


     return true;
}
