//  ************************************************************************************************
//
//  libformfactor: efficient and accurate computation of scattering form factors
//
//! @file      ff/EulerOperations.cpp
//! @brief     Implements atomic operations
//!
//! @homepage  https://jugit.fz-juelich.de/mlz/libformfactor
//! @license   GNU General Public License v3 or higher (see LICENSE)
//! @copyright Forschungszentrum Jülich GmbH 2022
//! @author    Joachim Wuttke, Scientific Computing Group at MLZ (see CITATION)
//
//  ************************************************************************************************

#include "ff/EulerOperations.h"
#include "ff/Edge.h"
#include "ff/Face.h"
#include "ff/Topology.h"
#include <algorithm>
#include <functional>
#include <unordered_map>

namespace {

//! Returns a pair std::pair<bool, R3>
//! - bool: a flag, it stores if the line is intersected.
//! - R3: the point of intersection.
//! This function is used for calculating the intersection of a plane with the offset
//! z_cut from the xy-plane with a line segment given by the vertices (v0,v1).

std::pair<bool, R3> edge_intersect(const R3& v0, const R3& v1, double z_cut)
{
    if (z_cut <= std::min(v0.z(), v1.z()) || z_cut >= std::max(v0.z(), v1.z()) || v0.z() == v1.z())
        return {false, R3()};
    R3 interpolated =
        (z_cut - v0.z()) / (v1.z() - v0.z()) * v1 + (v1.z() - z_cut) / (v1.z() - v0.z()) * v0;
    // only use x,y since z may != z_cut due to numerical imprecision
    return {true, {interpolated.x(), interpolated.y(), z_cut}};
}

//! Returns true if the xy-plane with the z_cut offset intersects with polyhedron.

bool assert_plane_cut(const ff::Polyhedron& polyhedron, double z_cut)
{
    bool above = false;
    bool below = false;
    for (const R3& v : polyhedron.vertices()) {
        if (v.z() > z_cut)
            above = true;
        if (v.z() < z_cut)
            below = true;
    }
    return above && below;
}

//! This function collects information about all edges that intersect with the clipping plane
//! Returns a vector of tuples:
//! - int: the vertex index in ff::FacialTopology::vertexIndices.
//! - int: the vertex index in ff::FacialTopology::vertexIndices.
//! - R3: the point of the intersection.

std::vector<std::tuple<int, int, R3>> findIntersectingEdges(const ff::Polyhedron& polyhedron,
                                                            double z_cut)
{
    const std::vector<ff::FacialTopology>& faces = polyhedron.topology().faces;
    const std::vector<R3>& vertices = polyhedron.vertices();
    std::vector<std::tuple<int, int, R3>> result;
    for (size_t iF = 0; iF < faces.size(); iF++) {
        std::vector<std::pair<int, int>> edges = faces[iF].edges_directed();
        // since mev creates a new edge edge_offset corrects the wrong indices
        for (size_t iE = 0; iE < edges.size(); iE++) {
            const auto [iE0, iE1] = edges[iE];
            if (const auto& [ok, intersection] =
                    edge_intersect(vertices[iE0], vertices[iE1], z_cut);
                ok) {
                auto itr =
                    std::find_if(result.begin(), result.end(), [iE0 = iE0, iE1 = iE1](auto tup) {
                        return (std::get<0>(tup) == iE0 && std::get<1>(tup) == iE1)
                               || (std::get<0>(tup) == iE1 && std::get<1>(tup) == iE0);
                    });
                if (itr == result.end())
                    result.emplace_back(iE0, iE1, intersection);
            }
        }
    }
    return result;
}

//! This function collects information about all faces that intersect with the clipping plane
//! Returns a vector of tuple:
//! - bool: a flag, it stores if a face was found.
//! - int: the face index in ff::Topology::faces.
//! - int: the edge index in ff::FacialTopology::vertexIndices of the first vertex intersecting.
//! - int: the edge index in ff::FacialTopology::vertexIndices of the second vertex intersecting.

std::tuple<bool, int, int, int> findIntersectingFaces(const ff::Polyhedron& polyhedron,
                                                      double z_cut)
{
    const std::vector<ff::FacialTopology>& faces = polyhedron.topology().faces;
    const std::vector<R3>& vertices = polyhedron.vertices();

    for (size_t iF = 0; iF < faces.size(); iF++) {
        std::vector<int> indices;
        const std::vector<int>& vertexIndices = faces[iF].vertexIndices;
        const int SZ = vertexIndices.size();
        if (SZ <= 3)
            continue;
        // collect vertex indices of vertices in z plane
        for (int iE = 0; iE < SZ; iE++) {
            if (vertices[vertexIndices[iE]].z() == z_cut)
                indices.push_back(iE);
        }
        if (indices.size() >= 2) {
            // check if edge exists already
            while (!indices.empty()
                   && ((indices[0] + 1) % SZ == indices[1]
                       || ((indices[0] - 1) % SZ + SZ) % SZ == indices[1])) {
                indices.erase(indices.begin());
                indices.erase(indices.begin());
            }
            if (indices.empty())
                continue;
            return {true, iF, indices[0], indices[1]};
        }
    }
    return {false, -1, -1, -1};
}

//! This function collects information about all edges starting from z_cut to some location
//! below/above z_cut Returns a vector of tuple:
//! - bool: a flag, it stores if an edge was found.
//! - int: the vertex index in ff::Polyhedron::vertices of the vertex with .z() == z_cut.
//! - int: the vertex index in ff::Polyhedron::vertices of the vertex with .z() < z_cut.
std::tuple<bool, int, int> findRemovableEdges(const ff::Polyhedron& polyhedron, double z_cut,
                                              bool findAbove)
{
    const std::vector<ff::FacialTopology>& faces = polyhedron.topology().faces;
    const std::vector<R3>& vertices = polyhedron.vertices();

    for (const ff::FacialTopology& ft : faces) {
        for (auto& [v0, v1] : ft.edges_directed()) {
            if (vertices[v0].z() == z_cut && vertices[v1].z() == z_cut)
                continue;
            if (findAbove) {
                if (vertices[v0].z() == z_cut && vertices[v1].z() > z_cut)
                    return {true, v0, v1};
                if (vertices[v1].z() == z_cut && vertices[v0].z() > z_cut)
                    return {true, v1, v0};
            } else {
                if (vertices[v0].z() == z_cut && vertices[v1].z() < z_cut)
                    return {true, v0, v1};
                if (vertices[v1].z() == z_cut && vertices[v0].z() < z_cut)
                    return {true, v1, v0};
            }
        }
    }
    return {false, -1, -1};
}

//! Returns a vector of face indices, where each face has all vertices located in the offset
//! z_cut xy-plane.

std::vector<int> faces_in_plane(const ff::Polyhedron& polyhedron, double z_cut)
{
    const std::vector<ff::FacialTopology>& faces = polyhedron.topology().faces;
    const std::vector<R3>& vertices = polyhedron.vertices();
    std::vector<int> result;

    for (auto ft = faces.begin(); ft != faces.end(); ft++) {
        bool allOnPlane = true;
        for (const int edge : ft->vertexIndices) {
            if (vertices[edge].z() != z_cut)
                allOnPlane = false;
        }
        if (allOnPlane)
            result.push_back(std::distance(faces.begin(), ft));
    }
    return result;
}

//! This method us used to find a common edge of two faces given by their indices iF0,iF1 in
//! ff::Topology::Faces.
//! Returns a tuple:
//! - bool: a flag, it stores if a common edge has been found.
//! - int: a vertex index of ff::Polyhedron::vertices
//! - int: a vertex index of ff::Polyhedron::vertices

std::tuple<bool, int, int> common_edge(const ff::Polyhedron& polyhedron, int iF0, int iF1)
{
    for (auto eF0 : polyhedron.topology().faces[iF0].edges_undirected()) {
        for (auto eF1 : polyhedron.topology().faces[iF1].edges_undirected()) {
            if (eF0 == eF1)
                return {true, eF0.first, eF0.second};
        }
    }
    return {false, -1, -1};
}

//! This function collects information about faces, with all vertices .z() == z_cut, that share an
//! edge. Returns a vector of tuple:
//! - bool: a flag, it stores if two faces were found.
//! - int: the face index in ff::Topology::faces
//! - int: the face index in ff::Topology::faces.

std::tuple<bool, int, int> findMergeableFaces(const ff::Polyhedron& polyhedron, double z_cut)
{
    const std::vector<int> faces = faces_in_plane(polyhedron, z_cut);
    for (int iF0 : faces) {
        for (int iF1 : faces) {
            if (iF0 == iF1)
                continue;
            if (const auto& [ok, iV0, iV1] = common_edge(polyhedron, iF0, iF1); ok)
                return {true, iV0, iV1};
        }
    }
    return {false, -1, -1};
}

//! Returns a pair of Topology and vector of vertices.
//! - Duplicate vertex ids in ff::FacialTopology::vertexIndices get removed.
//! - ff::Topology::faces erases ff::FacialTopology's with less than 3 vertices in
//! ff::FacialTopology::vertexIndices.
//! - All unused vertices of ff::Polyhedron::vertices get
//! removed and all vertices left get a new index assigned.

std::pair<ff::Topology, std::vector<R3>> strip(const ff::Topology& topology,
                                               const std::vector<R3>& vertices)
{
    // clear faces
    ff::Topology newTopology;
    newTopology.symmetry_Ci = false;
    for (auto ft = topology.faces.begin(); ft != topology.faces.end(); ft++) {
        std::vector<int> vertexIndices;
        for (auto iV = ft->vertexIndices.begin(); iV != ft->vertexIndices.end(); iV++) {
            if (iV == ft->vertexIndices.end() - 1) {
                if (*iV == *ft->vertexIndices.begin())
                    continue;
            } else if (*iV == *(iV + 1))
                continue;
            vertexIndices.push_back(*iV);
        }
        if (vertexIndices.size() < 3)
            continue;
        newTopology.faces.push_back({vertexIndices, false});
    }
    // creating the new vertex mapping by collecting all used vertices
    std::unordered_map<int, int> id_mapper;
    int next_new_id = 0;
    for (ff::FacialTopology& ft : newTopology.faces) {
        // rotate vertexIds vector to start with min element to stay as close as possible to old ids
        auto itr = std::min_element(ft.vertexIndices.begin(), ft.vertexIndices.end());
        std::rotate(ft.vertexIndices.begin(), itr, ft.vertexIndices.end());
        for (const int vertex_id : ft.vertexIndices) {
            if (id_mapper.count(vertex_id) == 0)
                id_mapper[vertex_id] = next_new_id++;
        }
    }
    // building the new vertex vector
    std::vector<R3> newVertices(id_mapper.size());
    for (const auto& [old_id, new_id] : id_mapper)
        newVertices[new_id] = vertices[old_id];
    // mapping to the new vertex ids
    for (ff::FacialTopology& ft : newTopology.faces) {
        for (int& vertex_id : ft.vertexIndices)
            vertex_id = id_mapper[vertex_id];
    }
    return {newTopology, newVertices};
}
} // namespace

//! Returns a new Polyhedron with modified faces. The face at index iF is divided along
//! a new edge (iE0,iE1) into two new faces. The old face gets replaced by
//! the first new face. The second new face gets pushed back in the face vector of Topology.

ff::Polyhedron* ff::atomic::mef(const Polyhedron& polyhedron, int iF, int iE0, int iE1)
{
    if (iF < 0 || iF >= (int)polyhedron.topology().faces.size())
        throw std::runtime_error("Invalid call to libformfactor, function mef: "
                                 "iF out of range");
    if (iE0 < 0 || iE0 >= (int)polyhedron.topology().faces[iF].vertexIndices.size())
        throw std::runtime_error("Invalid call to libformfactor, function mef: "
                                 "iE0 out of range");
    if (iE1 < 0 || iE1 >= (int)polyhedron.topology().faces[iF].vertexIndices.size())
        throw std::runtime_error("Invalid call to libformfactor, function mef: "
                                 "iE1 out of range");
    if (iE1 < iE0)
        std::swap(iE0, iE1);
    if (iE1 - iE0 <= 1)
        throw std::runtime_error("Invalid call to libformfactor, function mef: "
                                 "cannot make edge from indices");

    const std::vector<int>& vertexIndices = polyhedron.topology().faces[iF].vertexIndices;
    // create new edge loops
    std::vector<int> vI0;
    std::vector<int> vI1;
    bool left = true;
    for (size_t i = 0; i < vertexIndices.size(); i++) {
        const int iV = vertexIndices[i];
        if (left) {
            if ((int)i == iE0) {
                vI0.push_back(iV);
                vI1.push_back(iV);
                left = false;
            } else {
                vI0.push_back(iV);
            }
        } else {
            if ((int)i == iE1) {
                vI0.push_back(iV);
                vI1.push_back(iV);
                left = true;
            } else {
                vI1.push_back(iV);
            }
        }
    }
    Topology newTopology(polyhedron.topology());
    newTopology.symmetry_Ci = false;
    newTopology.faces[iF] = {vI1, false};
    newTopology.faces.push_back({vI0, false});

    return new Polyhedron(newTopology, polyhedron.vertices());
}

//! Returns a new Polyhedron with a the Vertex v will be
//! inserted in all edges between.

ff::Polyhedron* ff::atomic::mev(const Polyhedron& polyhedron, int iV0, int iV1, const R3& v)
{
    if (iV0 < 0 || iV0 >= (int)polyhedron.vertices().size())
        throw std::runtime_error("Invalid call to libformfactor, function mev: iV0 out of range");
    if (iV1 < 0 || iV1 >= (int)polyhedron.vertices().size())
        throw std::runtime_error("Invalid call to libformfactor, function mev: iV1 out of range");
    if (iV0 == iV1)
        throw std::runtime_error(
            "Invalid call to libformfactor, function mev: iV0 and iV1 are equal");

    std::vector<R3> newVertices(polyhedron.vertices());
    // find or insert new vertex
    int iV = -1;
    auto itrV = std::find(newVertices.begin(), newVertices.end(), v);

    if (itrV == newVertices.end()) {
        newVertices.push_back(v);
        iV = newVertices.size() - 1;
    } else {
        iV = std::distance(newVertices.begin(), itrV);
    }
    if (iV == iV0 || iV == iV1)
        throw std::runtime_error("Invalid call to libformfactor, function mev: "
                                 "new vertex is already part of the old edge");

    Topology newTopology(polyhedron.topology());
    newTopology.symmetry_Ci = false;

    for (FacialTopology& ft : newTopology.faces) {
        const std::vector<std::pair<int, int>> edges = ft.edges_undirected();
        for (size_t iE = 0; iE < edges.size(); iE++) {
            auto& [iv0, iv1] = edges[iE];
            if ((iv0 == iV0 && iv1 == iV1) || (iv0 == iV1 && iv1 == iV0)) {
                ft.vertexIndices.insert(ft.vertexIndices.begin() + iE + 1, iV);
                ft.symmetry_S2 = false;
                break;
            }
        }
    }

    return new Polyhedron(newTopology, newVertices);
}

//! KEF is an Euler Operator used to Kill one Edge one Face. The edge is given by the its vertex
//! identifiers iV0, iV1 which are the indices of ff::Polyhedron::vertices.

ff::Polyhedron* ff::atomic::kef(const Polyhedron& polyhedron, int iV0, int iV1)
{
    if (iV0 < 0 || iV0 >= (int)polyhedron.vertices().size())
        throw std::runtime_error("Invalid call to libformfactor, function kef: iV0 out of range");
    if (iV1 < 0 || iV1 >= (int)polyhedron.vertices().size())
        throw std::runtime_error("Invalid call to libformfactor, function kef: iV1 out of range");

    // find edge and prepare new topology
    int iF0 = -1;
    int iE0 = -1;
    int iF1 = -1;
    int iE1 = -1;

    Topology result;
    result.symmetry_Ci = false;

    const std::vector<FacialTopology>& faces = polyhedron.topology().faces;

    for (size_t iF = 0; iF < faces.size(); iF++) {
        const std::vector<int>& vertexIndices = polyhedron.topology().faces[iF].vertexIndices;
        const int NVI = vertexIndices.size();
        bool found = false;
        for (int iE = 0; iE < NVI; iE++) {
            if (vertexIndices[iE] == iV0 && vertexIndices[(iE + 1) % NVI] == iV1) {
                iF0 = iF;
                iE0 = iE;
                found = true;
            } else if (vertexIndices[iE] == iV1 && vertexIndices[(iE + 1) % NVI] == iV0) {
                iF1 = iF;
                iE1 = iE;
                found = true;
            }
        }
        if (!found)
            result.faces.push_back(faces[iF]);
    }

    if (iF0 == -1 || iF1 == -1 || iE0 == -1 || iE1 == -1)
        throw std::runtime_error("Bug in libformfactor: in function kef, E or F not found");
    if (iF0 == iF1)
        throw std::runtime_error("Bug in libformfactor: in function kef, F0 = F1");

    // merge both edge loops into vF0  connected by iV0, iV1
    std::vector<int> vF0(faces[iF0].vertexIndices);
    std::vector<int> vF1(faces[iF1].vertexIndices);
    std::rotate(vF0.begin(), vF0.begin() + iE0 + 1, vF0.end());
    std::rotate(vF1.begin(), vF1.begin() + iE1 + 1, vF1.end());
    vF0.insert(vF0.end(), vF1.begin() + 1, vF1.end() - 1);
    result.faces.push_back({vF0, false});

    return new Polyhedron(result, polyhedron.vertices());
}

//! Returns a new Polyhedron with modified faces: iV1 gets dissolved into iV0

ff::Polyhedron* ff::atomic::kev(const Polyhedron& polyhedron, int iV0, int iV1)
{
    if (iV0 < 0 || iV0 >= (int)polyhedron.vertices().size())
        throw std::runtime_error("Invalid call to libformfactor, function kev: iV0 out of range");
    if (iV1 < 0 || iV1 >= (int)polyhedron.vertices().size())
        throw std::runtime_error("Invalid call to libformfactor, function kev: iV0 out of range");

    bool found = false;
    Topology tmp(polyhedron.topology());
    tmp.symmetry_Ci = false;
    for (const FacialTopology& ft : tmp.faces) {
        for (auto& [iv0, iv1] : ft.edges_directed()) {
            if ((iv0 == iV1 && iv1 == iV1) || (iv0 == iV1 && iv1 == iV0))
                found = true;
        }
    }
    if (!found)
        throw std::runtime_error(
            "Invalid call to libformfactor, function kev: edge does not exist");

    for (FacialTopology& ft : tmp.faces) {
        for (int& iV : ft.vertexIndices)
            if (iV == iV1)
                iV = iV0;
    }
    const auto& [newTopology, newVertices] = strip(tmp, polyhedron.vertices());
    return new Polyhedron(newTopology, newVertices);
}

//! Returns a new polyhedron with the volume below R3{0,0,z_cut} removed.

ff::Polyhedron* ff::atomic::z_clip(const Polyhedron& polyhedron, double z_cut, bool keepBelow)
{
    auto result =
        std::make_unique<Polyhedron>(polyhedron.topology(), polyhedron.relative_vertices());
    z_cut = z_cut - polyhedron.location().z();
    if (!assert_plane_cut(*result, z_cut))
        return new ff::Polyhedron(result->topology(), result->relative_vertices(),
                                  polyhedron.location());
    for (auto& [iE0, iE1, v] : findIntersectingEdges(*result, z_cut))
        result.reset(mev(*result, iE0, iE1, v));
    for (auto&& [ok, iF, iE0, iE1] = findIntersectingFaces(*result, z_cut); ok;) {
        result.reset(mef(*result, iF, iE0, iE1));
        std::tie(ok, iF, iE0, iE1) = findIntersectingFaces(*result, z_cut);
    }
    for (auto&& [ok, iV0, iV1] = findRemovableEdges(*result, z_cut, keepBelow); ok;) {
        result.reset(kev(*result, iV0, iV1));
        std::tie(ok, iV0, iV1) = findRemovableEdges(*result, z_cut, keepBelow);
    }
    for (auto&& [ok, iF0, iF1] = findMergeableFaces(*result, z_cut); ok;) {
        result.reset(kef(*result, iF0, iF1));
        std::tie(ok, iF0, iF1) = findMergeableFaces(*result, z_cut);
    }
    result.reset(
        new ff::Polyhedron(result->topology(), result->relative_vertices(), polyhedron.location()));
    return result.release();
}
