#include "geometryMover.h"
#include "shapeAlgorithms.h"
#include <TopExp.hxx>
#include <BRepAlgoAPI_Fuse.hxx>
#include <BRepAlgoAPI_Cut.hxx>
#include <BRepBuilderAPI_Transform.hxx>
#include <TopExp_Explorer.hxx>
#include <BRepBuilderAPI_Copy.hxx>
#include "shapePartition.h"
#include "misc/miscFunctions.hpp"
#include "cenos_exception.h"
#include <set>

GeometryMover::GeometryMover()
{
}

GeometryMover::~GeometryMover()
{
}

void GeometryMover::setTransform(std::shared_ptr<TopoEntity> ent, gp_Trsf transform)
{
	entity_transform[ent] = transform;
}

void GeometryMover::moveGeometry(GeometryData_ geom_data)
{
    if (entity_transform.empty())
        return;


    for (auto& tr : entity_transform)
        full_entity_transform[tr.first] = tr.second;


    int dimension = geom_data->getDimension();

    geom_data->update(); // Orient faces for 2D cases
    TopoDS_Shape partition = geom_data->getShape();

    TopoDS_Shape fused_air_shape;
    BRepAlgoAPI_Fuse fuse;
    TopTools_ListOfShape toollist, arglist;
    
    for (auto& ent : geom_data->getEntities())
    {
        if (!ent->isEnabled())
            continue;

        if (ent->getDimension() == dimension)
        {
            toollist.Append(ent->getShape());
        }
    }
    arglist.Append(partition);
    fuse.SetArguments(arglist);
    fuse.SetTools(toollist);
    fuse.Build();
    fuse.SimplifyResult(true, true, Precision::Angular());
    fused_air_shape = fuse.Shape();

    //check number of subshapes
    TopTools_IndexedMapOfShape s1_faces, s2_faces, s1_edges, s2_edges, s1_solids, s2_solids;

    TopExp::MapShapes(partition, TopAbs_FACE, s1_faces);
    TopExp::MapShapes(partition, TopAbs_EDGE, s1_edges);
    TopExp::MapShapes(partition, TopAbs_SOLID, s1_solids);

    // ****************************************************
    // Fill vectors of moved, modified
    // ****************************************************

    std::vector<std::shared_ptr<TopoEntity>> moved_domains;
    std::vector<std::shared_ptr<TopoEntity>> modified_domains;
    std::vector<std::shared_ptr<TopoEntity>> moved_subentities;
    std::vector<std::shared_ptr<TopoEntity>> modified_subentities;
    // push in vector domain entities that will be moved
    for (auto& transformed_ent : entity_transform)
    {
        if (dimension == 3 and transformed_ent.first->getType() == ShapeType::SOLID)
        {
            moved_domains.push_back(transformed_ent.first);
        }
        else if (dimension == 2 and transformed_ent.first->getType() == ShapeType::FACE)
        {
            moved_domains.push_back(transformed_ent.first);
        }
    }

    auto relations = geom_data->getRelations();

    std::vector<std::shared_ptr<TopoEntity>> additional_moved_domains;
    for (auto mv_dom : moved_domains)
    {
        auto moved_boundaries = relations[mv_dom->getName()];

        //find domains attached to moved boundaries
        std::set<std::string> attached_domains;
        for (auto& mb : moved_boundaries)
        {
            for (auto ad : relations[mb])
                attached_domains.insert(ad);
        }


        for (auto& ad : attached_domains)
        {
            if (ad == mv_dom->getName())
                continue;

            auto& attached_boundaries = relations[ad];

            //find domains attached to attached boundaries
            std::set<std::string> attached_domains2;
            for (auto& ab : attached_boundaries)
            {
                for (auto& ad2 : relations[ab])
                    attached_domains2.insert(ad2);
            }

            // if attached_domains2 contains only mv_dom and ad,
            // ad should be moved!!
            if (attached_domains2.size() == 2 and attached_domains2.count(ad) == 1 and attached_domains2.count(mv_dom->getName()) == 1)
            {
                for (auto& ent : geom_data->getEntities())
                {
                    if (ent->getName() == ad)
                    {
                        additional_moved_domains.push_back(ent);
                        entity_transform[ent] = entity_transform[mv_dom];
                    }
                }
            }
        }
    }

    for (auto& amd : additional_moved_domains)
        moved_domains.push_back(amd);

    // push in vector domain entities that will be modified
    // all that are not moved are modified
    for (auto& ent : geom_data->getEntities())
    {
        if (!ent->isEnabled())
            continue;

        for (auto& mv_dom : moved_domains)
        {
            if (ent->getDimension() != dimension)
                continue;
            //skip moved entities
            if (std::find(moved_domains.begin(), moved_domains.end(), ent) != moved_domains.end())
                continue;
            if (!ent->isEnabled())
                continue;

            if (std::find(modified_domains.begin(), modified_domains.end(), ent) != modified_domains.end())
                continue;

            modified_domains.push_back(ent);
        }
    }

    //domains done

    // push lower type entities in moved entity vector
    for (auto& mv_dom : moved_domains)
    {
        for (auto& ent : geom_data->getEntities())
        {
            if (std::find(moved_subentities.begin(), moved_subentities.end(), ent) != moved_subentities.end())
                continue;
            if (!ent->isEnabled())
                continue;

            if (dimension == 3 and (ent->getType() == ShapeType::FACE or ent->getType() == ShapeType::EDGE))
            {
                if (ent->isSubShapeOf(mv_dom))
                {
                    moved_subentities.push_back(ent);
                    full_entity_transform[ent] = entity_transform[mv_dom];
                }
            }
            else  if (dimension == 2 and (ent->getType() == ShapeType::EDGE))
            {
                if (ent->isSubShapeOf(mv_dom))
                {
                    moved_subentities.push_back(ent);
                    full_entity_transform[ent] = entity_transform[mv_dom];
                }
            }
        }
    }

    // push rest of subentities in modified_subentities
    // and those that do not touch moved entities
    // into moved subentities with motion 0

    for (auto& ent : geom_data->getEntities())
    {
        if (dimension == 3 and ent->getType() == ShapeType::SOLID)
            continue;

        if (dimension == 2 and ent->getType() == ShapeType::FACE)
            continue;

        if (std::find(moved_subentities.begin(), moved_subentities.end(), ent) != moved_subentities.end())
            continue;

        if (std::find(modified_subentities.begin(), modified_subentities.end(), ent) != modified_subentities.end())
            continue;
        if (!ent->isEnabled())
            continue;

        // detect if entity touches any of moved domains.
        bool has_touch = false;
        for (auto& mv_ent : moved_domains)
        {
            if (ShapeAlgorithms::getMinDistance(ent->getShape(), mv_ent->getShape()) <= Precision::Confusion())
                has_touch = true;
        }

        if (has_touch)
        {
            modified_subentities.push_back(ent);
        }
        else
        {
            moved_subentities.push_back(ent);
            full_entity_transform[ent] = gp_Trsf();
        }
    }

   /*
    for (auto ent : moved_domains)
        std::cout << "moved_domains " << ent->getName() << std::endl;

    for (auto ent : modified_domains)
        std::cout << "modified_domains " << ent->getName() << std::endl;

    for (auto ent : moved_subentities)
        std::cout << "moved_subentities " << ent->getName() << std::endl;

    for (auto ent : modified_subentities)
        std::cout << "modified_subentities " << ent->getName() << std::endl;
    */

    // ****************************************************
    // Change shape for modified domains
    // ****************************************************

    // if more than one domain is modified, throw an error!
    // probably wrong use of geometryMover interface
    if (modified_domains.size() != 1)
        throw(cenos_exception("Cannot use automatic motion with this geometry. Check domain role assignement or contact support!"));

    TopoEntity_ modified_domain = modified_domains[0];

    TopoDS_Shape processed_shape = fused_air_shape;
    for (auto& mv_dom : moved_domains)
    {
        //cut transformed shape out of fused shape
        TopoDS_Shape tr_shape = mv_dom->getShape();

        BRepBuilderAPI_Transform tr_builder(tr_shape, entity_transform[mv_dom]);
        TopoDS_Shape cut = BRepAlgoAPI_Cut(processed_shape, tr_builder.Shape());
        processed_shape = cut;
    }

    if (dimension == 2)
    {
        TopExp_Explorer face_explorer(processed_shape, TopAbs_ShapeEnum::TopAbs_FACE);
        TopoDS_Shape new_shape = face_explorer.Current();
        if (modified_domain->getShape().Orientation() != new_shape.Orientation())
            new_shape.Reverse();
        modified_domain->setShape(new_shape);
    }
    else if (dimension == 3)
    {
        TopExp_Explorer solid_explorer(processed_shape, TopAbs_ShapeEnum::TopAbs_SOLID);
        TopoDS_Shape new_shape = solid_explorer.Current();
        if (modified_domain->getShape().Orientation() != new_shape.Orientation())
            new_shape.Reverse();
        modified_domain->setShape(new_shape);
    }


    // subshapes
    for (auto& md_ent : modified_subentities)
    {
        for (TopExp_Explorer subentity_explorer(modified_domain->getShape(), ShapeAlgorithms::getHighestShapeType(md_ent->getShape())); subentity_explorer.More(); subentity_explorer.Next())
        {
            TopoDS_Shape current = subentity_explorer.Current();

            if (ShapeAlgorithms::have_common(md_ent->getShape(), current))
            {
                if (md_ent->getShape().Orientation() != current.Orientation())
                    current.Reverse();

                md_ent->setShape(current);
                break;
            }
        }
    }

    // ****************************************************
    // Move moved entities and sub-entities
    // ****************************************************

    std::map<TopoEntity_, gp_Trsf> subentity_transform;
    std::vector<std::shared_ptr<TopoEntity>> processed_entities;
    for (auto& mv_dom : moved_domains)
    {
        // skip if already moved
        if (std::find(processed_entities.begin(), processed_entities.end(), mv_dom) != processed_entities.end())
            continue;

        //move corresponding sub-entities
        for (auto& mv_ent : moved_subentities)
        {
            // skip if already moved
            if (std::find(processed_entities.begin(), processed_entities.end(), mv_ent) != processed_entities.end())
                continue;

            if (mv_ent->isSubShapeOf(mv_dom))
            {
                subentity_transform[mv_ent] = entity_transform[mv_dom];
                TopoDS_Shape tr_shape = mv_ent->getShape();
                BRepBuilderAPI_Transform tr_builder(tr_shape, entity_transform[mv_dom]);
                TopoDS_Shape new_shape = tr_builder.Shape();
                if (tr_shape.Orientation() != new_shape.Orientation())
                    new_shape.Reverse();

                mv_ent->setShape(new_shape);
                processed_entities.push_back(mv_ent);
            }
        }

        // move entity
        TopoDS_Shape tr_shape = mv_dom->getShape();
        BRepBuilderAPI_Transform tr_builder(tr_shape, entity_transform[mv_dom]);
        TopoDS_Shape new_shape = tr_builder.Shape();
        if (tr_shape.Orientation() != new_shape.Orientation())
            new_shape.Reverse();

        mv_dom->setShape(new_shape);
        processed_entities.push_back(mv_dom);
    }


    // ****************************************************
    // Detect invalid motion
    // ****************************************************

    ShapeStats stat;
    ShapeAlgorithms::getShapeStatistics(&fused_air_shape, stat);
    for (auto mv_dom : moved_domains)
    {
        TopoDS_Shape tr_shape = mv_dom->getShape();
        BRepBuilderAPI_Copy shape_copy(tr_shape);
        ShapeStats move_stat;
        ShapeAlgorithms::getShapeStatistics(&shape_copy.Shape(), move_stat);

        if (stat.maxX - move_stat.maxX  < -100 * Precision::Confusion()
            or stat.maxY - move_stat.maxY  < -100 * Precision::Confusion()
            or stat.maxZ - move_stat.maxZ  < -100 * Precision::Confusion()
            or stat.minX - move_stat.minX  > 100 * Precision::Confusion()
            or stat.minY - move_stat.minY  > 100 * Precision::Confusion()
            or stat.minZ - move_stat.minZ > 100 * Precision::Confusion()  )
        {
            if (wDir != "")
            {
                misc::createFolder(wDir + "/debug");
                ShapeAlgorithms::writeBrepFile(tr_shape, wDir + "/debug/moved_shape.brep");
                ShapeAlgorithms::writeBrepFile(fused_air_shape, wDir + "/debug/air_shape_fused.brep");
            }
            std::string msg = "Moving parts go out of simulation domain! Please check your input!";
            throw(cenos_exception(msg));
        }


        //check for collisions with other moving parts
        for (auto mv_dom_xxx : moved_domains)
        {
            //do not compare with self
            if (mv_dom_xxx == mv_dom)
                continue;

            //we initially may have collision(s) with other parts. as long as we have shared edges (moving as a whole),
            //we do not consider this to be a collision.
            if (mv_dom_xxx->hasSharedEdges(mv_dom))
                continue;

            TopoDS_Shape tr_shape_xxx = mv_dom_xxx->getShape();

            if (ShapeAlgorithms::getMinDistance(tr_shape, tr_shape_xxx) <= Precision::Confusion())
            {
                if (wDir != "")
                {
                    misc::createFolder(wDir + "/debug");
                    ShapeAlgorithms::writeBrepFile(tr_shape, wDir + "/debug/collided_shape_1.brep");
                    ShapeAlgorithms::writeBrepFile(tr_shape_xxx, wDir + "/debug/collided_shape_2.brep");
                }

                std::string msg = "Collision detected in between moving parts (->" + mv_dom_xxx->getName() + " <---> " + mv_dom->getName() + "<-)! Please check your input!";
                throw(cenos_exception(msg));
            }

        }
    }


    //**************************************************************************************
    //                                  END OF MOTION DFETECT 
    //**************************************************************************************

    // ****************************************************
    // Re-calculate partition
    // ****************************************************
    ShapePartition part(geom_data->getEntities());
    for (auto& ent : geom_data->getEntities())
    {
        if (ent->getShape().IsNull())
            throw(cenos_exception("Wrong shape of entity " + ent->getName() + " during motion. Check your input and contact support!"));
    }

    geom_data->recalculateShape();
    partition = geom_data->getShape();

    TopExp::MapShapes(partition, TopAbs_FACE, s2_faces);
    TopExp::MapShapes(partition, TopAbs_EDGE, s2_edges);
    TopExp::MapShapes(partition, TopAbs_SOLID, s2_solids);

    std::string error_msg = "";
    if (s1_solids.Extent() != s2_solids.Extent())
        error_msg = "GM Collision detected / topology changed for `solids` during motion. Please check your input !";
    if (s1_faces.Extent() != s2_faces.Extent())
        error_msg = "GM Collision detected / topology changed for `faces` during motion. Please check your input !";
    if (s1_edges.Extent() != s2_edges.Extent())
        error_msg = "GM Collision detected / topology changed for `edges` during motion. Please check your input !";

    if (error_msg != "")
    {
        if (wDir != "")
        {
            misc::createFolder(wDir + "/debug");
            ShapeAlgorithms::writeBrepFile(partition, wDir + "/debug/partition_shape.brep");
            throw(cenos_exception(error_msg));
        }
    }

}

std::map<TopoEntity_, gp_Trsf> GeometryMover::getFullTransform()
{
    return full_entity_transform;
}

