#include "OFMesh_IO.h"
#include "cenos_exception.h"
#include "misc/miscFunctions.hpp"

#include "misc/Timer.hpp"
#include <unordered_map>


#define OF_HEADER(stream, classStr, objStr)  stream << "//*******************************************\n";\
stream << "//                                           \n";\
stream << "//        Mesh converted by CENOS            \n";\
stream << "//                                           \n";\
stream << "//*******************************************\n";\
stream << "	FoamFile\n";\
stream << "	{\n"; \
stream << "		version		2.0;\n"; \
stream << "		format		ascii;\n"; \
stream << "		class		" << classStr << ";\n"; \
stream << "		location	\"constant / polyMesh\";\n"; \
stream << "		object		" << objStr << ";\n";\
stream << "	}\n";


class vector_with_hash : public std::vector<int> {
public:
    size_t hash;
    vector_with_hash(const std::vector<int> &v) : std::vector<int>(v)
    {
        sort(begin(), end());
        hash = 7654321;
        for (int i : v)
        {
            hash ^= i * 31;
        }
    }
    vector_with_hash(const vector_with_hash &v) : std::vector<int>(v), hash(v.hash)
    { }
    vector_with_hash()
    { }
};

namespace std {
    template<>
    struct hash<vector_with_hash> {
        size_t operator ()(const vector_with_hash &v) const {
            return v.hash;
        }
    };
}

OFMesh_IO::OFMesh_IO()
{
}

void OFMesh_IO::readMesh(std::string& filename, Mesh* mesh)
{
}

void OFMesh_IO::writeMesh(std::string& dirname, Mesh* mesh)
{
    misc::createFolder(dirname);
    misc::createFolder(dirname + "/polyMesh");
    Timer t;
    t.start("calculateFaces");
    calculateFaces(mesh);
    t.stop();

    t.start("writePoints");

    writePoints(dirname + "/polyMesh/points", mesh);
    t.stop();

     writeFaces(dirname + "/polyMesh/faces", mesh);
    writeOwner(dirname + "/polyMesh/owner", mesh);
    writeNeighbour(dirname + "/polyMesh/neighbour", mesh);
    writeBoundary(dirname + "/polyMesh/boundary", mesh);

}



void OFMesh_IO::writePoints(std::string& filename, Mesh* mesh)
{
    std::ofstream file_stream;
    file_stream.open(filename.c_str());
    if (!file_stream.is_open())
    {
        throw cenos_exception("Point file cannot be opened");
    }

    OF_HEADER(file_stream, "vectorField",  "points");
    file_stream << "\n\n";

    std::set<Node> nodeset;
    for (auto ent : mesh->getEntities())
    {
        if (std::find(restricted_entity_names.begin(), restricted_entity_names.end(), ent->getName()) != restricted_entity_names.end())
        {
            auto nds = ent->getNodes();
            for (auto n : nds)
            {
                nodeset.insert(*n);
            }
        }
    }

    if (nodeset.empty())
    {
        for (auto n : mesh->getNodes())
        {
            nodeset.insert(*n);
        }
    }

    int count = 0;
    double scale = mesh->getMeshScale();
    file_stream << nodeset.size() << "\n(\n";
    for (auto n : nodeset)
    {
        file_stream << "    (  " << n.x() * scale << "    " << n.y() * scale << "    " << n.z() * scale << "  )\n";
        nodes_for_of[n.getId()] = count;
        count++;
    }

    file_stream << ")\n";
    file_stream.close();

}


void OFMesh_IO::writeFaces(std::string& filename, Mesh* mesh)
{
    std::ofstream file_stream;
    file_stream.open(filename.c_str());
    if (!file_stream.is_open())
    {
        throw cenos_exception("Face file cannot be opened");
    }

    OF_HEADER(file_stream, "faceList", "faces");
    file_stream << "\n\n";

    file_stream << faces.size() << "\n(\n";
    for (auto f : faces)
    {
        auto pts = f.getPoints();
       
        file_stream << pts.size() << " (  ";
        for (auto p : pts)
            file_stream << nodes_for_of[p] << "    ";
        file_stream << " )\n";
    }
    file_stream << ")\n";
    file_stream.close();

}



void OFMesh_IO::writeOwner(std::string& filename, Mesh* mesh)
{
    std::ofstream file_stream;
    file_stream.open(filename.c_str());
    if (!file_stream.is_open())
    {
        throw cenos_exception("Owner file cannot be opened");
    }

    OF_HEADER(file_stream, "labelList", "owner");
    file_stream << "\n\n";

    file_stream << faces.size() << "\n(\n";

    int count = 0;
    for (auto f : faces)
    {
        file_stream << cells_for_of[f.getOwner()] << "\n";
    }

    file_stream << ")\n";
    file_stream.close();

}


void OFMesh_IO::writeNeighbour(std::string& filename, Mesh* mesh)
{
    std::ofstream file_stream;
    file_stream.open(filename.c_str());
    if (!file_stream.is_open())
    {
        throw cenos_exception("Neighbour file cannot be opened");
    }

    OF_HEADER(file_stream, "labelList", "neighbour");
    file_stream << "\n\n";

    //count internal faces
    int int_count = 0;
    for (auto f : faces)
        if (!f.isBoundaryFace())
            int_count++;

    file_stream << int_count << "\n(\n";
    for (auto f : faces)
    {
        if (!f.isBoundaryFace())
            file_stream << cells_for_of[f.getNeighbour()] << "\n";
    }
    file_stream << ")\n";
    file_stream.close();
}



void OFMesh_IO::writeBoundary(std::string& filename, Mesh* mesh)
{
    std::ofstream file_stream;
    file_stream.open(filename.c_str());
    if (!file_stream.is_open())
    {
        throw cenos_exception("Boundary file cannot be opened");
    }

    OF_HEADER(file_stream, "polyBoundaryMesh", "boundary");
    file_stream << "\n\n";

    file_stream << boundary_names.size() << "\n(\n";
    for (int i = 0; i < boundary_names.size(); i++)
    {
        std::string name = boundary_names[i];
        file_stream << name << "\n";
        file_stream << "{\n";
        file_stream << "    type    patch;\n";
        file_stream << "    nFaces    " << boundary_lenght[name] << ";\n";
        file_stream << "    startFace    " << boundary_start[name] << ";\n";
        file_stream << "}\n";
    }
    file_stream << ")\n";
    file_stream.close();

}

void OFMesh_IO::calculateFaces(Mesh* mesh)
{
    faces.clear();

    Timer t;
    t.start("cal 1");
    int cell_count = 0;
    std::unordered_map<vector_with_hash, int> all_faces;
    for (auto ent : mesh->getEntities())
    {
        if (std::find(restricted_entity_names.begin(), restricted_entity_names.end(), ent->getName()) != restricted_entity_names.end())
        {
            auto els = ent->getElements();
            for (auto e : els)
            {

                if (e->getDimension() != 3)
                    continue;
                cells_for_of[e->getId()] = cell_count;
                cell_count++;

                for (auto f : e->getFaces())
                {
                    // check if such face already exists!
                    vector_with_hash vwh(f.getPoints());
                    auto it = all_faces.find(vwh);
                    
                    if (it != all_faces.end())
                    {
                        faces[it->second].setNeighbour(e->getId());
                    }
                    else
                    {
                        all_faces[vwh] = (int)faces.size();
                        faces.push_back(f);
                    }

                }
            }
        }
    }
    t.stop();
    t.start("cal 2");
    if (faces.empty())
    {
        for (auto e : mesh->getElements())
        {
            if (e->getDimension() != 3)
                continue;
            cells_for_of[e->getId()] = cell_count;
            cell_count++;

            for (auto f : e->getFaces())
            {
                vector_with_hash vwh(f.getPoints());
                auto it = all_faces.find(vwh);

                if (it != all_faces.end())
                {
                    faces[it->second].setNeighbour(e->getId());
                }
                else
                {
                    all_faces[vwh] = (int)faces.size();
                    faces.push_back(f);
                }
            }
        }
    }

    t.stop();
    //***************
    // assign boundary names to faces
    t.start("cal 3");
    for (auto e : mesh->getElements())
    {
        if (e->getDimension() != 2)
            continue;

        for (auto f : e->getFaces())
        {
            vector_with_hash vwh(f.getPoints());
            auto it = all_faces.find(vwh);
            if (it != all_faces.end())
            {
                auto ent = e->getOwnerEntity();
                faces[it->second].setBoundary(ent->getName());
                auto in = std::find(boundary_names.begin(), boundary_names.end(), ent->getName());
                if (in == boundary_names.end())
                    boundary_names.push_back(ent->getName());
            }
        }
    }
    //**************
    t.stop();

    t.start("cal 4");
    std::vector<Face> reordered_faces;
    int count = 0;
    for (auto& f : faces)
    {
        if (!f.isBoundaryFace())
        {
            reordered_faces.push_back(f);
            count++;
        }
    }
    t.stop();
    

    t.start("cal 5");
    std::sort(reordered_faces.begin(), reordered_faces.end(), [this](Face const& f1, Face const& f2) { return cells_for_of[f1.getOwner()] < cells_for_of[f2.getOwner()]; });
    std::sort(reordered_faces.begin(), reordered_faces.end(), [this](Face const& f1, Face const& f2) { return cells_for_of[f1.getNeighbour()] < cells_for_of[f2.getNeighbour()]; });

    t.stop();

    t.start("cal 6");
    // write boundary faces
    for (auto name : boundary_names)
    {
        boundary_start[name] = count;
        int b_count = 0;
        for (auto f : faces)
        {
            if (f.getBoundary() == name)
            {
                reordered_faces.push_back(f);
                count++;
                b_count++;
            }
        }
        boundary_lenght[name] = b_count;
    }

    t.stop();
    faces = reordered_faces;
}
