// Copyright (c) 2025-2026, SFCGAL Team.
// SPDX-License-Identifier: LGPL-2.0-or-later

#include "SFCGAL/algorithm/surfaceSimplification.h"

#include "SFCGAL/GeometryCollection.h"
#include "SFCGAL/Kernel.h"
#include "SFCGAL/MultiSolid.h"
#include "SFCGAL/PolyhedralSurface.h"
#include "SFCGAL/Solid.h"
#include "SFCGAL/Surface.h"
#include "SFCGAL/Triangle.h"
#include "SFCGAL/TriangulatedSurface.h"
#include "SFCGAL/algorithm/isValid.h"

#include <CGAL/Surface_mesh.h>
#include <CGAL/Surface_mesh_simplification/Edge_collapse_visitor_base.h>
#include <CGAL/Surface_mesh_simplification/Policies/Edge_collapse/Edge_count_ratio_stop_predicate.h>
#include <CGAL/Surface_mesh_simplification/Policies/Edge_collapse/Edge_count_stop_predicate.h>
#include <CGAL/Surface_mesh_simplification/Policies/Edge_collapse/Edge_length_cost.h>
#include <CGAL/Surface_mesh_simplification/Policies/Edge_collapse/Midpoint_placement.h>
#include <CGAL/Surface_mesh_simplification/edge_collapse.h>

#ifdef SFCGAL_WITH_EIGEN
  #include <CGAL/Cartesian_converter.h>
  #include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
  #include <CGAL/Surface_mesh_simplification/Policies/Edge_collapse/GarlandHeckbert_plane_policies.h>
  #include <CGAL/Surface_mesh_simplification/Policies/Edge_collapse/LindstromTurk_cost.h>
  #include <CGAL/Surface_mesh_simplification/Policies/Edge_collapse/LindstromTurk_placement.h>
#endif // SFCGAL_WITH_EIGEN

#include <map>
#include <stdexcept>

namespace SFCGAL::algorithm {

namespace SMS = CGAL::Surface_mesh_simplification;

namespace detail {

/**
 * @brief Apply edge collapse simplification using Edge Length cost and Midpoint
 * placement
 */
template <typename StopPredicate>
auto
defaultEdgeCollapser(Surface_mesh_3 &mesh, const StopPredicate &stop) -> size_t
{
  using Cost      = SMS::Edge_length_cost<Surface_mesh_3>;
  using Placement = SMS::Midpoint_placement<Surface_mesh_3>;

  return SMS::edge_collapse(
      mesh, stop,
      CGAL::parameters::get_cost(Cost()).get_placement(Placement()));
}

#ifdef SFCGAL_WITH_EIGEN
// Inexact kernel types for advanced strategies
using InexactKernel = CGAL::Exact_predicates_inexact_constructions_kernel;
using InexactMesh   = CGAL::Surface_mesh<InexactKernel::Point_3>;
using EK_to_IK      = CGAL::Cartesian_converter<Kernel, InexactKernel>;
using IK_to_EK      = CGAL::Cartesian_converter<InexactKernel, Kernel>;

/**
 * @brief Apply edge collapse simplification using Garland-Heckbert strategy on
 * inexact mesh
 */
template <typename StopPredicate>
auto
garlandHeckbertEdgeCollapser(InexactMesh &mesh, const StopPredicate &stop)
    -> size_t
{
  using Policies =
      SMS::GarlandHeckbert_plane_policies<InexactMesh, InexactKernel>;
  Policies policies(mesh);

  return SMS::edge_collapse(mesh, stop,
                            CGAL::parameters::get_cost(policies.get_cost())
                                .get_placement(policies.get_placement()));
}

/**
 * @brief Apply edge collapse simplification using Lindstrom-Turk strategy on
 * inexact mesh
 */
template <typename StopPredicate>
auto
lindstromTurkEdgeCollapser(InexactMesh &mesh, const StopPredicate &stop)
    -> size_t
{
  using Cost      = SMS::LindstromTurk_cost<InexactMesh>;
  using Placement = SMS::LindstromTurk_placement<InexactMesh>;

  return SMS::edge_collapse(
      mesh, stop,
      CGAL::parameters::get_cost(Cost()).get_placement(Placement()));
}

/**
 * @brief Simplify inexact meshes with Eigen strategies.
 */
template <typename EdgeCollapseFn>
auto
applyInexactEdgeCollapserWithStopPredicate(
    Surface_mesh_3 &mesh, const SimplificationStopPredicate &stop,
    EdgeCollapseFn &&edgeCollapseFn) -> size_t
{
  constexpr size_t MAX_VERTICES = 500000;
  constexpr size_t MAX_FACES    = 1000000;

  if (mesh.number_of_vertices() > MAX_VERTICES ||
      mesh.number_of_faces() > MAX_FACES) {
    throw std::invalid_argument("Mesh too large for kernel conversion - use "
                                "EDGE_LENGTH strategy instead");
  }

  EK_to_IK    toInexact;
  IK_to_EK    toExact;
  InexactMesh inexactMesh;

  std::map<Surface_mesh_3::Vertex_index, InexactMesh::Vertex_index> vmap;

  for (auto vertices : mesh.vertices()) {
    vmap[vertices] = inexactMesh.add_vertex(toInexact(mesh.point(vertices)));
  }

  for (auto faces : mesh.faces()) {
    std::vector<InexactMesh::Vertex_index> verts;
    for (auto face_vertices : mesh.vertices_around_face(mesh.halfedge(faces))) {
      verts.push_back(vmap[face_vertices]);
    }
    inexactMesh.add_face(verts);
  }

  size_t result =
      applyEdgeCollapserWithStopPredicate(inexactMesh, stop, edgeCollapseFn);

  mesh.clear();
  std::map<InexactMesh::Vertex_index, Surface_mesh_3::Vertex_index> backMap;

  for (auto vertices : inexactMesh.vertices()) {
    backMap[vertices] = mesh.add_vertex(toExact(inexactMesh.point(vertices)));
  }

  for (auto faces : inexactMesh.faces()) {
    std::vector<Surface_mesh_3::Vertex_index> verts;
    for (auto face_vertices :
         inexactMesh.vertices_around_face(inexactMesh.halfedge(faces))) {
      verts.push_back(backMap[face_vertices]);
    }
    mesh.add_face(verts);
  }

  return result;
}
#endif // SFCGAL_WITH_EIGEN

/**
 * @brief Call a simplification function on a mesh (exact or inexact) depending
 * on an edge count stop predicate.
 */
template <typename Mesh, typename EdgeCollapseFn>
auto
applyEdgeCollapserThroughEdgeCount(Mesh &mesh, const size_t targetEdgeCount,
                                   EdgeCollapseFn &&edgeCollapseFn) -> size_t
{
  auto const initialEdgeCount = static_cast<size_t>(mesh.number_of_edges());

  if (targetEdgeCount >= initialEdgeCount) {
    return static_cast<size_t>(0);
  }

  SMS::Edge_count_stop_predicate<Mesh> predicate(targetEdgeCount);
  return edgeCollapseFn(mesh, predicate);
}

/**
 * @brief Call a simplification function on a mesh (exact or inexact) depending
 * on an edge count ratio stop predicate.
 */
template <typename Mesh, typename EdgeCollapseFn>
auto
applyEdgeCollapserThroughEdgeRatio(Mesh &mesh, const double &ratio,
                                   EdgeCollapseFn &&edgeCollapseFn) -> size_t
{
  if (ratio <= 0.0 || ratio >= 1.0) {
    throw std::invalid_argument(
        "Edge count ratio must be in the range (0.0, 1.0)");
  }
  SMS::Edge_count_ratio_stop_predicate<Mesh> predicate(ratio);
  return edgeCollapseFn(mesh, predicate);
}

/**
 * @brief Call a simplification function on a mesh (exact or inexact) depending
 * on a stop predicate.
 *
 * The function handles edge count as well as edge ratio stop predicates.
 */
template <typename Mesh, typename EdgeCollapseFn>
auto
applyEdgeCollapserWithStopPredicate(Mesh                              &mesh,
                                    const SimplificationStopPredicate &stop,
                                    EdgeCollapseFn &&edgeCollapseFn) -> size_t
{
  size_t result;
  if (stop.type == SimplificationStopPredicate::PredicateType::EDGE_COUNT) {
    auto const targetEdgeCount = static_cast<size_t>(stop.value);
    result = applyEdgeCollapserThroughEdgeCount(mesh, targetEdgeCount,
                                                edgeCollapseFn);
  } else {
    double const ratio = stop.value;
    result = applyEdgeCollapserThroughEdgeRatio(mesh, ratio, edgeCollapseFn);
  }
  return result;
}

/**
 * @brief Entry point for simplifying surface meshes
 *
 * This function encapsulates the selection of the accurate simplification
 * function depending on the chosen strategy. The regular edge length strategy
 * is used as a default case, whilst the Garland-Heckbert and Lindstrom-Turk
 * strategies require the SFCGAL_WITH_EIGEN compilation option. Both strategies
 * trigger a proxy function that deals with a copied mesh computed onto an
 * inexact CGAL kernel.
 *
 *
 */
auto
simplifySurfaceMesh(Surface_mesh_3                    &mesh,
                    const SimplificationStopPredicate &stop,
                    SimplificationStrategy             strategy) -> size_t
{

  switch (strategy) {
#ifdef SFCGAL_WITH_EIGEN
  case SimplificationStrategy::GARLAND_HECKBERT: {
    auto edgeCollapseFn = [&](auto &mesh_, auto &predicate) {
      return garlandHeckbertEdgeCollapser(mesh_, predicate);
    };
    return applyInexactEdgeCollapserWithStopPredicate(mesh, stop,
                                                      edgeCollapseFn);
  }
  case SimplificationStrategy::LINDSTROM_TURK: {
    auto edgeCollapseFn = [&](auto &mesh_, auto &predicate) {
      return lindstromTurkEdgeCollapser(mesh_, predicate);
    };
    return applyInexactEdgeCollapserWithStopPredicate(mesh, stop,
                                                      edgeCollapseFn);
  }
#else
    (void)strategy;
#endif // SFCGAL_WITH_EIGEN
  default: {
    auto edgeCollapseFn = [&](Surface_mesh_3 &mesh_, const auto &predicate) {
      return defaultEdgeCollapser(mesh_, predicate);
    };
    return applyEdgeCollapserWithStopPredicate(mesh, stop, edgeCollapseFn);
  }
  }
}

/**
 * @brief Simplify a TriangulatedSurface
 */
auto
simplifyTriangulatedSurface(const TriangulatedSurface         &surface,
                            const SimplificationStopPredicate &stopPredicate,
                            SimplificationStrategy             strategy)
    -> std::unique_ptr<TriangulatedSurface>
{
  if (surface.isEmpty()) {
    return std::make_unique<TriangulatedSurface>(surface);
  }

  // Convert to Surface_mesh
  Surface_mesh_3 mesh = surface.toSurfaceMesh();

  // Simplify
  simplifySurfaceMesh(mesh, stopPredicate, strategy);

  // Convert back to PolyhedralSurface first, then to TriangulatedSurface
  PolyhedralSurface polyhedral(mesh);
  return std::make_unique<TriangulatedSurface>(
      polyhedral.toTriangulatedSurface());
}

/**
 * @brief Simplify a PolyhedralSurface
 */
auto
simplifyPolyhedralSurface(const PolyhedralSurface           &surface,
                          const SimplificationStopPredicate &stopPredicate,
                          SimplificationStrategy             strategy)
    -> std::unique_ptr<PolyhedralSurface>
{
  if (surface.isEmpty()) {
    return std::make_unique<PolyhedralSurface>(surface);
  }

  // Convert to Surface_mesh
  Surface_mesh_3 mesh = surface.toSurfaceMesh();

  // Simplify
  simplifySurfaceMesh(mesh, stopPredicate, strategy);

  // Convert back to PolyhedralSurface
  return std::make_unique<PolyhedralSurface>(mesh);
}

/**
 * @brief Simplify a Solid (only exterior shell)
 */
auto
simplifySolid(const Solid                       &solid,
              const SimplificationStopPredicate &stopPredicate,
              SimplificationStrategy strategy) -> std::unique_ptr<Solid>
{
  if (solid.isEmpty()) {
    return std::make_unique<Solid>(solid);
  }

  // Simplify the exterior shell
  auto simplifiedExterior =
      simplifyPolyhedralSurface(solid.exteriorShell(), stopPredicate, strategy);

  // Create new solid with simplified exterior shell
  auto result = std::make_unique<Solid>(std::move(simplifiedExterior));

  // Copy interior shells (not simplified)
  for (size_t i = 0; i < solid.numInteriorShells(); ++i) {
    result->addInteriorShell(solid.interiorShellN(i).clone());
  }

  return result;
}

/**
 * @brief Simplify a MultiSolid
 */
auto
simplifyMultiSolid(const MultiSolid                  &multiSolid,
                   const SimplificationStopPredicate &stopPredicate,
                   SimplificationStrategy             strategy)
    -> std::unique_ptr<MultiSolid>
{
  auto result = std::make_unique<MultiSolid>();

  for (size_t i = 0; i < multiSolid.numGeometries(); ++i) {
    const auto &solid = multiSolid.geometryN(i).as<Solid>();
    result->addGeometry(simplifySolid(solid, stopPredicate, strategy));
  }

  return result;
}

} // namespace detail

/**
 * @brief Simplify a surface mesh using CGAL edge collapse algorithm
 * @param geometry The input geometry to simplify (must be a surface or solid)
 * @param stopPredicate When to stop the simplification process
 * @param strategy The cost and placement strategy to use
 * @return A simplified copy of the input geometry
 * @pre The input geometry must be valid and non-empty
 * @pre For EDGE_COUNT_RATIO, the ratio must be in the range (0.0, 1.0)
 * @pre The geometry must be 3-dimensional
 */
auto
surfaceSimplification(const Geometry                    &geometry,
                      const SimplificationStopPredicate &stopPredicate,
                      SimplificationStrategy             strategy)
    -> std::unique_ptr<Geometry>
{
  // Validate geometry
  switch (geometry.geometryTypeId()) {
  case TYPE_TRIANGULATEDSURFACE:
  case TYPE_POLYHEDRALSURFACE:
  case TYPE_SOLID:
  case TYPE_MULTISOLID:
    SFCGAL_ASSERT_GEOMETRY_VALIDITY_3D(geometry);
    break;
  default:
    break;
  }

  std::unique_ptr<Geometry> result(surfaceSimplification(
      geometry, stopPredicate, strategy, NoValidityCheck()));
  propagateValidityFlag(*result, true);
  return result;
}

auto
surfaceSimplification(const Geometry                    &geometry,
                      const SimplificationStopPredicate &stopPredicate,
                      SimplificationStrategy             strategy,
                      NoValidityCheck /*unused*/) -> std::unique_ptr<Geometry>
{
  if (geometry.isEmpty()) {
    return geometry.clone();
  }

  // Validate ratio for EDGE_COUNT_RATIO
  if (stopPredicate.type ==
      SimplificationStopPredicate::PredicateType::EDGE_COUNT_RATIO) {
    if (stopPredicate.value <= 0.0 || stopPredicate.value >= 1.0) {
      throw std::invalid_argument(
          "Edge count ratio must be in the range (0.0, 1.0)");
    }
  }

  switch (geometry.geometryTypeId()) {
  case TYPE_TRIANGULATEDSURFACE:
    return detail::simplifyTriangulatedSurface(
        geometry.as<TriangulatedSurface>(), stopPredicate, strategy);

  case TYPE_POLYHEDRALSURFACE:
    return detail::simplifyPolyhedralSurface(geometry.as<PolyhedralSurface>(),
                                             stopPredicate, strategy);

  case TYPE_SOLID:
    return detail::simplifySolid(geometry.as<Solid>(), stopPredicate, strategy);

  case TYPE_MULTISOLID:
    return detail::simplifyMultiSolid(geometry.as<MultiSolid>(), stopPredicate,
                                      strategy);

  default:
    throw std::invalid_argument(
        "surfaceSimplification only supports TriangulatedSurface, "
        "PolyhedralSurface, Solid, and MultiSolid geometries");
  }
}

} // namespace SFCGAL::algorithm
