// Copyright (c) 2023 GeometryFactory
// All rights reserved.
//
// This file is part of CGAL (www.cgal.org).
//
// $URL: https://github.com/CGAL/cgal/blob/v6.0.3/Straight_skeleton_extrusion_2/include/CGAL/extrude_skeleton.h $
// $Id: include/CGAL/extrude_skeleton.h cefe3007d59 $
// SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-Commercial
//
// Author(s)     : Mael Rouxel-Labbé
//

#ifndef CGAL_SLS_EXTRUSION_EXTRUDE_SKELETON_H
#define CGAL_SLS_EXTRUSION_EXTRUDE_SKELETON_H

#include <CGAL/license/Straight_skeleton_extrusion_2.h>

#ifdef CGAL_SLS_DEBUG_DRAW
  #include <CGAL/draw_straight_skeleton_2.h>
  #include <CGAL/draw_polygon_2.h>
  #include <CGAL/draw_polygon_with_holes_2.h>
  #include <CGAL/draw_triangulation_2.h>
#endif

#include <CGAL/create_weighted_straight_skeleton_2.h>
#include <CGAL/create_weighted_straight_skeleton_from_polygon_with_holes_2.h>
#include <CGAL/create_weighted_offset_polygons_2.h>
#include <CGAL/create_weighted_offset_polygons_from_polygon_with_holes_2.h>
#include <CGAL/Straight_skeleton_2/Straight_skeleton_aux.h>

#include <CGAL/General_polygon_with_holes_2.h>

#include <CGAL/Polygon_mesh_processing/border.h>
#include <CGAL/Polygon_mesh_processing/polygon_soup_to_polygon_mesh.h>
#include <CGAL/Polygon_mesh_processing/repair_polygon_soup.h>
#include <CGAL/Polygon_mesh_processing/stitch_borders.h>

#ifdef CGAL_SLS_OUTPUT_FILES
#include <CGAL/IO/polygon_soup_io.h>
#endif

#include <CGAL/Constrained_Delaunay_triangulation_2.h>
#include <CGAL/Projection_traits_3.h>
#include <CGAL/Triangulation_vertex_base_with_info_2.h>
#include <CGAL/mark_domain_in_triangulation.h>

#include <CGAL/Cartesian/function_objects.h>
#include <CGAL/enum.h>
#include <CGAL/IO/helpers.h>
#include <CGAL/Kernel_traits.h>
#include <CGAL/Polygon_2.h>

#include <boost/algorithm/clamp.hpp>
#include <boost/range/value_type.hpp>

#include <algorithm>
#include <iostream>
#include <map>
#include <type_traits>
#include <unordered_map>
#include <vector>
#include <optional>
#include <memory>

namespace CGAL {
namespace Straight_skeleton_extrusion {
namespace internal {

////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////
// Below is to handle vertical slabs.

enum class Slope
{
  UNKNOWN = 0,
  INWARD,
  OUTWARD,
  VERTICAL
};

template <typename FT>
inline constexpr FT default_extrusion_height()
{
  return (std::numeric_limits<double>::max)();
}

// @todo Maybe this postprocessing is not really necessary? Do users really care if the point
// is not perfectly above the input contour edge (it generally cannot be anyway if the kernel
// is not exact except for some specific cases)?
#define CGAL_SLS_SNAP_TO_VERTICAL_SLABS
#ifdef CGAL_SLS_SNAP_TO_VERTICAL_SLABS

// The purpose of this visitor is to snap back almost-vertical (see preprocessing_weights()) edges
// to actual vertical slabs.
template <typename HDS, typename GeomTraits>
typename GeomTraits::Point_2
snap_point_to_contour_halfedge_plane(const typename GeomTraits::Point_2& op,
                                     typename HDS::Halfedge_const_handle ch)
{
  using FT = typename GeomTraits::FT;
  using Segment_2 = typename GeomTraits::Segment_2;
  using Line_2 = typename GeomTraits::Line_2;

  using Vertex_const_handle = typename HDS::Vertex_const_handle;

  const Vertex_const_handle sv = ch->opposite()->vertex();
  const Vertex_const_handle tv = ch->vertex();

  if(sv->point().x() == tv->point().x())
  {
    // vertical edge
    // std::cout << "vertical edge, snapping " << op << " to " << sv->point().x() << " "  << op.y() << std::endl;
    return { sv->point().x(), op.y() };
  }
  else if(sv->point().y() == tv->point().y())
  {
    // horizontal edge
    // std::cout << "horizontal edge, snapping " << op << " to " << op.x() << " " << sv->point().y() << std::endl;
    return { op.x(), sv->point().y() };
  }
  else
  {
    // Project orthogonally onto the halfedge
    // @todo should the projection be along the direction of the other offset edge sharing this point?
    Segment_2 s { sv->point(), tv->point() };
    std::optional<Line_2> line = CGAL_SS_i::compute_normalized_line_coeffC2(s);
    CGAL_assertion(bool(line)); // otherwise the skeleton would have failed already

    FT px, py;
    CGAL::line_project_pointC2(line->a(),line->b(),line->c(), op.x(),op.y(), px,py);
    // std::cout << "snapping " << op << " to " << px << " " << py << std::endl;
    return { px, py };
  }
}

template <typename HDS, typename GeomTraits>
void snap_skeleton_vertex(typename HDS::Halfedge_const_handle hds_h,
                          typename HDS::Halfedge_const_handle contour_h,
                          std::map<typename GeomTraits::Point_2,
                                   typename GeomTraits::Point_2>& snapped_positions)
{
  typename HDS::Vertex_const_handle hds_tv = hds_h->vertex();

  // this re-applies snapping towards contour_h even if the point was already snapped towards another contour
  auto insert_result = snapped_positions.emplace(hds_tv->point(), hds_tv->point());
  insert_result.first->second = snap_point_to_contour_halfedge_plane<HDS, GeomTraits>(insert_result.first->second, contour_h);

  // std::cout << "snap_skeleton_vertex(V" << hds_tv->id() << " pt: " << hds_h->vertex()->point() << ")"
  //           << " to " << insert_result.first->second << std::endl;
}

template <typename GeomTraits, typename PointRange>
void apply_snapping(PointRange& points,
                    const std::map<typename GeomTraits::Point_2,
                                   typename GeomTraits::Point_2>& snapped_positions)
{
  using Point_3 = typename GeomTraits::Point_3;

  for(Point_3& p3 : points)
  {
    auto it = snapped_positions.find({ p3.x(), p3.y() });
    if(it != snapped_positions.end())
      p3 = Point_3{it->second.x(), it->second.y(), p3.z()};
  }
}
#endif // CGAL_SLS_SNAP_TO_VERTICAL_SLABS

template <typename StraightSkeleton_2,
          typename OffsetBuilderTraits,
          typename GeomTraits>
class Skeleton_offset_correspondence_builder_visitor
  : public CGAL::Default_polygon_offset_builder_2_visitor<OffsetBuilderTraits, StraightSkeleton_2>
{
  using Base = CGAL::Default_polygon_offset_builder_2_visitor<OffsetBuilderTraits, StraightSkeleton_2>;

  using FT = typename GeomTraits::FT;
  using Point_2 = typename GeomTraits::Point_2;

  using SS_Halfedge_const_handle = typename StraightSkeleton_2::Halfedge_const_handle;

  using HDS = typename StraightSkeleton_2::Base;
  using HDS_Halfedge_const_handle = typename HDS::Halfedge_const_handle;

public:
  Skeleton_offset_correspondence_builder_visitor(const StraightSkeleton_2& ss,
                                                 std::unordered_map<HDS_Halfedge_const_handle, Point_2>& offset_points
#ifdef CGAL_SLS_SNAP_TO_VERTICAL_SLABS
                                               , const FT vertical_weight
                                               , std::map<Point_2, Point_2>& snapped_positions
#endif
                                                )
    : m_ss(ss)
    , m_offset_points(offset_points)
#ifdef CGAL_SLS_SNAP_TO_VERTICAL_SLABS
    , m_vertical_weight(vertical_weight)
    , m_snapped_positions(snapped_positions)
#endif
  { }

public:
  void on_offset_contour_started() const
  {
    // std::cout << "~~ new contour ~~" << std::endl;
  }

  // can't modify the position yet because we need arrange_polygons() to still work properly
  //
  // @fixme on paper one could create a polygon thin-enough w.r.t. the max weight value
  // such thatthere is a skeleton vertex that wants to be snapped to two different sides...
  void on_offset_point(const Point_2& op,
                       SS_Halfedge_const_handle hook) const
  {
    CGAL_precondition(hook->is_bisector());

    HDS_Halfedge_const_handle canonical_hook = (hook < hook->opposite()) ? hook : hook->opposite();
    m_offset_points[canonical_hook] = op;

#ifdef CGAL_SLS_SNAP_TO_VERTICAL_SLABS
    CGAL_precondition(m_snapped_positions.count(op) == 0);

    SS_Halfedge_const_handle contour_h1 = hook->defining_contour_edge();
    CGAL_assertion(contour_h1->opposite()->is_border());
    SS_Halfedge_const_handle contour_h2 = hook->opposite()->defining_contour_edge();
    CGAL_assertion(contour_h2->opposite()->is_border());

    const bool is_h1_vertical = (contour_h1->weight() == m_vertical_weight);
    const bool is_h2_vertical = (contour_h2->weight() == m_vertical_weight);

    // if both are vertical, it's the common vertex (which has to exist)
    if(is_h1_vertical && is_h2_vertical)
    {
      CGAL_assertion(contour_h1->vertex() == contour_h2->opposite()->vertex() ||
                     contour_h2->vertex() == contour_h1->opposite()->vertex());
      if(contour_h1->vertex() == contour_h2->opposite()->vertex())
        m_snapped_positions[op] = contour_h1->vertex()->point();
      else
        m_snapped_positions[op] = contour_h2->vertex()->point();
    }
    else if(is_h1_vertical)
    {
      m_snapped_positions[op] = snap_point_to_contour_halfedge_plane<HDS, GeomTraits>(op, contour_h1);
    }
    else if(is_h2_vertical)
    {
      m_snapped_positions[op] = snap_point_to_contour_halfedge_plane<HDS, GeomTraits>(op, contour_h2);
    }
#endif
  }

private:
  const StraightSkeleton_2& m_ss;
  std::unordered_map<HDS_Halfedge_const_handle, Point_2>& m_offset_points;

#ifdef CGAL_SLS_SNAP_TO_VERTICAL_SLABS
  const FT m_vertical_weight;
  std::map<Point_2, Point_2>& m_snapped_positions;
#endif
};

////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////

template <typename GeomTraits>
class Extrusion_builder
{
  using Geom_traits = GeomTraits;

  using FT = typename Geom_traits::FT;
  using Point_2 = typename Geom_traits::Point_2;
  using Segment_2 = typename Geom_traits::Segment_2;
  using Line_2 = typename Geom_traits::Line_2;
  using Point_3 = typename Geom_traits::Point_3;
  using Vector_3 = typename Geom_traits::Vector_3;

  using Polygon_2 = CGAL::Polygon_2<Geom_traits>;
  using Polygon_with_holes_2 = CGAL::Polygon_with_holes_2<Geom_traits>;

  using Offset_polygons = std::vector<std::shared_ptr<Polygon_2> >;
  using Offset_polygons_with_holes = std::vector<std::shared_ptr<Polygon_with_holes_2> >;

  using Straight_skeleton_2 = CGAL::Straight_skeleton_2<Geom_traits>;
  using Straight_skeleton_2_ptr = std::shared_ptr<Straight_skeleton_2>;

  using SS_Vertex_const_handle = typename Straight_skeleton_2::Vertex_const_handle;
  using SS_Halfedge_const_handle = typename Straight_skeleton_2::Halfedge_const_handle;

  using HDS = typename Straight_skeleton_2::Base;
  using HDS_Vertex_const_handle = typename HDS::Vertex_const_handle;
  using HDS_Halfedge_const_handle = typename HDS::Halfedge_const_handle;
  using HDS_Face_handle = typename HDS::Face_handle;

  // Standard CDT2 for the horizontal (z constant) faces
  using Vb = CGAL::Triangulation_vertex_base_with_info_2<std::size_t, Geom_traits>;
  using Vbb = CGAL::Triangulation_vertex_base_2<Geom_traits, Vb>;
  using Fb = CGAL::Constrained_triangulation_face_base_2<Geom_traits>;
  using TDS = CGAL::Triangulation_data_structure_2<Vb,Fb>;
  using Itag = CGAL::No_constraint_intersection_requiring_constructions_tag;
  using CDT = CGAL::Constrained_Delaunay_triangulation_2<Geom_traits, TDS, Itag>;
  using CDT_Vertex_handle = typename CDT::Vertex_handle;
  using CDT_Face_handle = typename CDT::Face_handle;

  // Projection CDT2 for the lateral faces
  using PK = CGAL::Projection_traits_3<Geom_traits>;
  using PVbb = CGAL::Triangulation_vertex_base_with_info_2<std::size_t, PK>;
  using PVb = CGAL::Triangulation_vertex_base_2<PK, PVbb>;
  using PFb = CGAL::Constrained_triangulation_face_base_2<PK>;
  using PTDS = CGAL::Triangulation_data_structure_2<PVb,PFb>;
  using PCDT = CGAL::Constrained_Delaunay_triangulation_2<PK, PTDS, Itag>;
  using PCDT_Vertex_handle = typename PCDT::Vertex_handle;
  using PCDT_Face_handle = typename PCDT::Face_handle;

  using Offset_builder_traits = CGAL::Polygon_offset_builder_traits_2<Geom_traits>;
  using Visitor = Skeleton_offset_correspondence_builder_visitor<Straight_skeleton_2, Offset_builder_traits, Geom_traits>;
  using Offset_builder = CGAL::Polygon_offset_builder_2<Straight_skeleton_2,
                                                        Offset_builder_traits,
                                                        Polygon_2,
                                                        Visitor>;

private:
  Geom_traits m_gt;

public:
  Extrusion_builder(const Geom_traits& gt) : m_gt(gt) { }

  ////////////////////////////////////////////////////////////////////////////////////////////////////
  ////////////////////////////////////////////////////////////////////////////////////////////////////
  ////////////////////////////////////////////////////////////////////////////////////////////////////

  template <typename PolygonWithHoles,
            typename PointRange, typename FaceRange>
  void construct_horizontal_faces(const PolygonWithHoles& p,
                                  const FT altitude,
                                  PointRange& points,
                                  FaceRange& faces,
                                  const bool invert_faces = false)
  {
#ifdef CGAL_SLS_DEBUG_DRAW
    CGAL::draw(p);
#endif

    CDT cdt;

    try
    {
      cdt.insert_constraint(p.outer_boundary().begin(), p.outer_boundary().end(), true /*close*/);
      for(auto h_it=p.holes_begin(); h_it!=p.holes_end(); ++h_it)
        cdt.insert_constraint(h_it->begin(), h_it->end(), true /*close*/);
    }
    catch(const typename CDT::Intersection_of_constraints_exception&)
    {
      std::cerr << "Warning: Failed to triangulate horizontal face" << std::endl;
      return;
    }

    std::size_t id = points.size(); // point ID offset (previous faces inserted their points)
    for(CDT_Vertex_handle vh : cdt.finite_vertex_handles())
    {
      points.emplace_back(cdt.point(vh).x(), cdt.point(vh).y(), altitude);
      vh->info() = id++;
    }

#ifdef CGAL_SLS_DEBUG_DRAW
    // CGAL::draw(cdt);
#endif

    std::unordered_map<CDT_Face_handle, bool> in_domain_map;
    boost::associative_property_map< std::unordered_map<CDT_Face_handle, bool> > in_domain(in_domain_map);

    CGAL::mark_domain_in_triangulation(cdt, in_domain);

    for(CDT_Face_handle f : cdt.finite_face_handles())
    {
      if(!get(in_domain, f))
        continue;

      // invert faces for the z=0 plane (bottom face)
      if(invert_faces)
        faces.push_back({f->vertex(0)->info(), f->vertex(2)->info(), f->vertex(1)->info()});
      else
        faces.push_back({f->vertex(0)->info(), f->vertex(1)->info(), f->vertex(2)->info()});
    }
  }

  template <typename PointRange, typename FaceRange>
  void construct_horizontal_faces(const Offset_polygons_with_holes& p_ptrs,
                                  const FT altitude,
                                  PointRange& points,
                                  FaceRange& faces)
  {
    for(const auto& p_ptr : p_ptrs)
      construct_horizontal_faces(*p_ptr, altitude, points, faces);
  }

  template <typename SLSFacePoints, typename PointRange, typename FaceRange>
  void triangulate_skeleton_face(SLSFacePoints& face_points,
                                 const bool invert_faces,
                                 PointRange& points,
                                 FaceRange& faces)
  {
    CGAL_precondition(face_points.size() >= 3);

    // shift once to ensure that face_points[0] and face_points[1] are at z=0 and thus the normal is correct
    std::rotate(face_points.rbegin(), face_points.rbegin() + 1, face_points.rend());
    CGAL_assertion(face_points[0][2] == 0 && face_points[1][2] == 0);

    const Vector_3 n = CGAL::cross_product(face_points[1] - face_points[0], face_points[2] - face_points[0]);
    PK traits(n);
    PCDT pcdt(traits);

    try
    {
      pcdt.insert_constraint(face_points.begin(), face_points.end(), true /*close*/);
    }
    catch(const typename PCDT::Intersection_of_constraints_exception&)
    {
      std::cerr << "Warning: Failed to triangulate skeleton face" << std::endl;
      return;
    }

    std::size_t id = points.size(); // point ID offset (previous faces inserted their points);
    for(PCDT_Vertex_handle vh : pcdt.finite_vertex_handles())
    {
      points.push_back(pcdt.point(vh));
      vh->info() = id++;
    }

#ifdef CGAL_SLS_DEBUG_DRAW
    // CGAL::draw(pcdt);
#endif

    std::unordered_map<PCDT_Face_handle, bool> in_domain_map;
    boost::associative_property_map< std::unordered_map<PCDT_Face_handle, bool> > in_domain(in_domain_map);

    CGAL::mark_domain_in_triangulation(pcdt, in_domain);

    for(PCDT_Face_handle f : pcdt.finite_face_handles())
    {
      if(!get(in_domain, f))
        continue;

      // invert faces for exterior skeletons
      if(invert_faces)
        faces.push_back({f->vertex(0)->info(), f->vertex(2)->info(), f->vertex(1)->info()});
      else
        faces.push_back({f->vertex(0)->info(), f->vertex(1)->info(), f->vertex(2)->info()});
    }
  }

  // This version is for default height, so just gather all the full faces
  void construct_lateral_faces(const Straight_skeleton_2& ss,
                               std::vector<Point_3>& points,
                               std::vector<std::vector<std::size_t> >& faces,
#ifdef CGAL_SLS_SNAP_TO_VERTICAL_SLABS
                               const FT& vertical_weight,
                               std::map<Point_2, Point_2>& snapped_positions,
#endif
                               const bool ignore_frame_faces = false,
                               const bool invert_faces = false)
  {
    const HDS& hds = static_cast<const HDS&>(ss);

    std::size_t fc = 0;

    for(const HDS_Face_handle hds_f : CGAL::faces(hds))
    {
      std::vector<Point_3> face_points;

      // If they exist (exterior skeleton), the first four faces of the SLS correspond
      // to the outer frame, and should be ignored.
      if(ignore_frame_faces && fc++ < 4)
        continue;

      HDS_Halfedge_const_handle hds_h = hds_f->halfedge(), done = hds_h;
#ifdef CGAL_SLS_SNAP_TO_VERTICAL_SLABS
      HDS_Halfedge_const_handle contour_h = hds_h->defining_contour_edge();
      CGAL_assertion(hds_h == contour_h);
      const bool is_vertical = (contour_h->weight() == vertical_weight);
#endif

      do
      {
        HDS_Vertex_const_handle hds_tv = hds_h->vertex();

#ifdef CGAL_SLS_SNAP_TO_VERTICAL_SLABS
        // this computes the snapped position but does not change the geometry of the skeleton
        if(is_vertical && !hds_tv->is_contour())
          snap_skeleton_vertex<HDS, Geom_traits>(hds_h, contour_h, snapped_positions);
#endif

        face_points.emplace_back(hds_tv->point().x(), hds_tv->point().y(), hds_tv->time());

        hds_h = hds_h->next();
      }
      while(hds_h != done);

      if(face_points.size() < 3)
      {
        std::cerr << "Warning: sm_vs has size 1 or 2: offset crossing face at a single point?" << std::endl;
        continue;
      }

      triangulate_skeleton_face(face_points, invert_faces, points, faces);
    }
  }

  void construct_lateral_faces(const Straight_skeleton_2& ss,
                              const Offset_builder& offset_builder,
                              const FT height,
                              std::vector<Point_3>& points,
                              std::vector<std::vector<std::size_t> >& faces,
                              const std::unordered_map<HDS_Halfedge_const_handle, Point_2>& offset_points,
#ifdef CGAL_SLS_SNAP_TO_VERTICAL_SLABS
                              const FT& vertical_weight,
                              std::map<Point_2, Point_2>& snapped_positions,
#endif
                              const bool ignore_frame_faces = false,
                              const bool invert_faces = false)
  {
    CGAL_precondition(height != default_extrusion_height<FT>());

    const bool extrude_upwards = is_positive(height);
    const FT abs_height = CGAL::abs(height);

    const HDS& hds = static_cast<const HDS&>(ss);

    std::size_t fc = 0;
    for(const HDS_Face_handle hds_f : CGAL::faces(hds))
    {
      // If they exist (exterior skeleton), the first four faces of the SLS correspond
      // to the outer frame, and should be ignored.
      if(ignore_frame_faces && fc++ < 4)
        continue;

      std::vector<Point_3> face_points;

      HDS_Halfedge_const_handle hds_h = hds_f->halfedge(), done = hds_h;

#ifdef CGAL_SLS_SNAP_TO_VERTICAL_SLABS
      HDS_Halfedge_const_handle contour_h = hds_h->defining_contour_edge();
      CGAL_assertion(hds_h == contour_h);
      const bool is_vertical = (contour_h->weight() == vertical_weight);
#endif

      do
      {
        HDS_Vertex_const_handle hds_sv = hds_h->opposite()->vertex();
        HDS_Vertex_const_handle hds_tv = hds_h->vertex();

        // Compare_offset_against_event_time compares offset to node->time(),
        // so when the offset is greater or equal than the node->time(), the node is the face
        auto compare_time_to_offset = [&](HDS_Vertex_const_handle node) -> CGAL::Comparison_result
        {
          if(node->is_contour())
            return CGAL::LARGER; // offset > 0 and contour nodes' time is 0
          else
            return offset_builder.Compare_offset_against_event_time(abs_height, node);
        };

        const CGAL::Comparison_result sc = compare_time_to_offset(hds_sv);
        const CGAL::Comparison_result tc = compare_time_to_offset(hds_tv);

        // if the offset is crossing at the source, it will be added when seen as a target
        // from the previous halfedge

        if(sc != tc && sc != CGAL::EQUAL && tc != CGAL::EQUAL)
        {
          // std::cout << "sc != tc" << std::endl;
          CGAL_assertion(sc != CGAL::EQUAL && tc != CGAL::EQUAL);

          HDS_Halfedge_const_handle hds_off_h = hds_h;
          if(hds_h->slope() == CGAL::NEGATIVE) // ensure same geometric point on both sides
            hds_off_h = hds_off_h->opposite();

          // The offset point must already been computed in the offset builder visitor
          auto off_p = offset_points.find(hds_off_h);
          CGAL_assertion(off_p != offset_points.end());

          face_points.emplace_back(off_p->second.x(), off_p->second.y(), height);
        }

        if(tc != CGAL::SMALLER)
        {
#ifdef CGAL_SLS_SNAP_TO_VERTICAL_SLABS
          if(is_vertical && !hds_tv->is_contour())
            snap_skeleton_vertex<HDS, Geom_traits>(hds_h, contour_h, snapped_positions);
#endif

          const Point_2& off_p = hds_tv->point();
          // ->time() could be an approximation of the true time. If we are here, the target's time
          // is smaller than the height, but we still sanitize it in case of numerical errors in ->time().
          const FT time = boost::algorithm::clamp(hds_tv->time(), FT(0), abs_height);
          face_points.emplace_back(off_p.x(), off_p.y(), extrude_upwards ? time : - time);
        }

        hds_h = hds_h->next();
      }
      while(hds_h != done);

      if(face_points.size() < 3)
      {
        std::cerr << "Warning: sm_vs has size 1 or 2: offset crossing face at a single point?" << std::endl;
        continue;
      }

      triangulate_skeleton_face(face_points, invert_faces, points, faces);
    }
  }

public:
  // this is roughly "CGAL::create_interior_weighted_skeleton_and_offset_polygons_with_holes_2()",
  // but we want to know the intermediate straight skeleton to build the lateral faces of the 3D mesh
  template <typename PolygonWithHoles,
            typename PointRange, typename FaceRange>
  bool inward_construction(const PolygonWithHoles& pwh,
                          const std::vector<std::vector<FT> >& speeds,
                          const FT vertical_weight,
                          const FT height,
                          PointRange& points,
                          FaceRange& faces)
  {
    CGAL_precondition(!is_zero(height));

    const FT abs_height = abs(height);

    // bottom face (z=0)
    construct_horizontal_faces(pwh, 0 /*height*/, points, faces, true /*invert faces*/);

    // Avoid recomputing offset points multiple times
    std::unordered_map<HDS_Halfedge_const_handle, Point_2> offset_points;

#ifdef CGAL_SLS_SNAP_TO_VERTICAL_SLABS
    // This is to deal with vertical slabs: we temporarily give a non-vertical slope to be able
    // to construct the SLS, and we then snap back the position to the vertical planes.
    // Note that points in non-vertical faces are also changed a bit
    std::map<Point_2, Point_2> snapped_positions;
#endif

    Straight_skeleton_2_ptr ss_ptr;

    bool is_default_extrusion_height = (height == default_extrusion_height<FT>());

    // @partial_wsls_pwh interior SLS of weighted polygons with holes can have skeleton faces with holes
    // The current postprocessing is in the function EnforceSimpleConnectedness, but it has not yet
    // been made compatible with partial skeletons.
    if(is_default_extrusion_height || pwh.number_of_holes() != 0)
    {
      ss_ptr = CGAL::create_interior_weighted_straight_skeleton_2(
                 CGAL_SS_i::vertices_begin(pwh.outer_boundary()),
                 CGAL_SS_i::vertices_end(pwh.outer_boundary()),
                 pwh.holes_begin(), pwh.holes_end(),
                 std::begin(speeds[0]), std::end(speeds[0]),
                 std::next(std::begin(speeds)), std::end(speeds),
                 m_gt);
    }
    else
    {
      ss_ptr = CGAL_SS_i::create_partial_interior_weighted_straight_skeleton_2(
                 abs_height,
                 CGAL_SS_i::vertices_begin(pwh.outer_boundary()),
                 CGAL_SS_i::vertices_end(pwh.outer_boundary()),
                 pwh.holes_begin(), pwh.holes_end(),
                 std::begin(speeds[0]), std::end(speeds[0]),
                 std::next(std::begin(speeds)), std::end(speeds),
                 m_gt);
    }

    if(!ss_ptr)
    {
      std::cerr << "Error: encountered an error during skeleton construction" << std::endl;
      return false;
    }

#ifdef CGAL_SLS_DEBUG_DRAW
    // print_straight_skeleton(*ss_ptr);
    CGAL::draw(*ss_ptr);
#endif

    if(is_default_extrusion_height)
    {
#ifdef CGAL_SLS_SNAP_TO_VERTICAL_SLABS
      construct_lateral_faces(*ss_ptr, points, faces, vertical_weight, snapped_positions);
#else
      construct_lateral_faces(*ss_ptr, points, faces);
#endif
    }
    else // height != default_extrusion_height<FT>()
    {
#ifdef CGAL_SLS_SNAP_TO_VERTICAL_SLABS
      Visitor visitor(*ss_ptr, offset_points, vertical_weight, snapped_positions);
#else
      Visitor visitor(*ss_ptr, offset_points);
#endif
      Offset_builder ob(*ss_ptr, Offset_builder_traits(), visitor);
      Offset_polygons raw_output;
      ob.construct_offset_contours(abs_height, std::back_inserter(raw_output));

      Offset_polygons_with_holes output = CGAL::arrange_offset_polygons_2<Polygon_with_holes_2>(raw_output);
      construct_horizontal_faces(output, height, points, faces);

#ifdef CGAL_SLS_SNAP_TO_VERTICAL_SLABS
      construct_lateral_faces(*ss_ptr, ob, height, points, faces, offset_points, vertical_weight, snapped_positions);
#else
      construct_lateral_faces(*ss_ptr, ob, height, points, faces, offset_points);
#endif
    }

    // if the height is negative (extruding downwards), we need to invert all the faces
    if(is_negative(height))
    {
      for(auto& f : faces)
        std::swap(f[0], f[1]);
    }

#ifdef CGAL_SLS_SNAP_TO_VERTICAL_SLABS
    apply_snapping<Geom_traits>(points, snapped_positions);
#endif

    return true;
  }

  template <typename PolygonWithHoles,
            typename PointRange, typename FaceRange>
  bool outward_construction(const PolygonWithHoles& pwh,
                            const std::vector<std::vector<FT> >& speeds,
                            const FT vertical_weight,
                            const FT height,
                            PointRange& points,
                            FaceRange& faces)
  {
    CGAL_precondition(!is_zero(height));
    CGAL_precondition(height != default_extrusion_height<FT>()); // was checked before, this is just a reminder

    const FT abs_height = abs(height);

    // bottom face (z=0)
    construct_horizontal_faces(pwh, 0 /*height*/, points, faces, true /*invert faces*/);

    // Avoid recomputing offset points multiple times
    std::unordered_map<HDS_Halfedge_const_handle, Point_2> offset_points;

#ifdef CGAL_SLS_SNAP_TO_VERTICAL_SLABS
    // This is to deal with vertical slabs: we temporarily give a non-vertical slope to be able
    // to construct the SLS, and we then snap back the position to the vertical planes.
    // Note that points in non-vertical faces are also changed a bit
    std::map<Point_2, Point_2> snapped_positions;
#endif

    Offset_polygons raw_output; // accumulates for both the outer boundary and the holes

    // the exterior of a polygon with holes is the exterior of its outer boundary,
    // and the interior of its inverted holes
    //
    // Start with the outer boundary
    {
      Straight_skeleton_2_ptr ss_ptr = CGAL_SS_i::create_partial_exterior_weighted_straight_skeleton_2(
                                          abs_height,
                                          CGAL_SS_i::vertices_begin(pwh.outer_boundary()),
                                          CGAL_SS_i::vertices_end(pwh.outer_boundary()),
                                          std::begin(speeds[0]), std::end(speeds[0]),
                                          m_gt);

      if(!ss_ptr)
      {
        std::cerr << "Error: encountered an error during outer skeleton construction" << std::endl;
        return false;
      }

#ifdef CGAL_SLS_DEBUG_DRAW
      // print_straight_skeleton(*ss_ptr);
      CGAL::draw(*ss_ptr);
#endif

#ifdef CGAL_SLS_SNAP_TO_VERTICAL_SLABS
      Visitor visitor(*ss_ptr, offset_points, vertical_weight, snapped_positions);
#else
      Visitor visitor(*ss_ptr, offset_points);
#endif
      Offset_builder ob(*ss_ptr, Offset_builder_traits(), visitor);
      ob.construct_offset_contours(abs_height, std::back_inserter(raw_output));

      // Manually filter the offset of the outer frame
      std::swap(raw_output[0], raw_output.back());
      raw_output.pop_back();

#ifdef CGAL_SLS_SNAP_TO_VERTICAL_SLABS
      construct_lateral_faces(*ss_ptr, ob, height, points, faces, offset_points,
                              vertical_weight, snapped_positions,
                              true /*ignore frame faces*/, true /*invert faces*/);
#else
      construct_lateral_faces(*ss_ptr, ob, height, points, faces, offset_points,
                              true /*ignore frame faces*/, true /*invert faces*/);
#endif
    }

    // now, deal with the holes

    std::size_t hole_id = 0;
    for(auto hit=pwh.holes_begin(); hit!=pwh.holes_end(); ++hit, ++hole_id)
    {
      Polygon_2 hole = *hit; // intentional copy
      hole.reverse_orientation();

      // this is roughly "CGAL::create_exterior_weighted_skeleton_and_offset_polygons_with_holes_2()",
      // but we want to know the intermediate straight skeleton to build the lateral faces of the 3D mesh

      std::vector<Polygon_2> no_holes;
      std::vector<std::vector<FT> > no_speeds;
      Straight_skeleton_2_ptr ss_ptr = CGAL_SS_i::create_partial_interior_weighted_straight_skeleton_2(
                                          abs_height,
                                          CGAL_SS_i::vertices_begin(hole),
                                          CGAL_SS_i::vertices_end(hole),
                                          std::begin(no_holes), std::end(no_holes),
                                          std::begin(speeds[1 + hole_id]), std::end(speeds[1 + hole_id]),
                                          std::begin(no_speeds), std::end(no_speeds),
                                          m_gt);

      if(!ss_ptr)
      {
        std::cerr << "Error: encountered an error during skeleton construction" << std::endl;
        return EXIT_FAILURE;
      }

#ifdef CGAL_SLS_DEBUG_DRAW
      // print_straight_skeleton(*ss_ptr);
      CGAL::draw(*ss_ptr);
#endif

#ifdef CGAL_SLS_SNAP_TO_VERTICAL_SLABS
      Visitor visitor(*ss_ptr, offset_points, vertical_weight, snapped_positions);
#else
      Visitor visitor(*ss_ptr, offset_points);
#endif
      Offset_builder ob(*ss_ptr, Offset_builder_traits(), visitor);
      ob.construct_offset_contours(abs_height, std::back_inserter(raw_output));

#ifdef CGAL_SLS_SNAP_TO_VERTICAL_SLABS
      construct_lateral_faces(*ss_ptr, ob, height, points, faces, offset_points,
                              vertical_weight, snapped_positions,
                              false /*no outer frame*/, true /*invert faces*/);
#else
      construct_lateral_faces(*ss_ptr, ob, height, points, faces, offset_points,
                              false /*no outer frame*/, true /*invert faces*/);
#endif
    }

    // - the exterior offset of the outer boundary is built by creating an extra frame and turning
    // the outer boundary into a hole. Hence, it needs to be reversed back to proper orientation
    // - the exterior offset of the holes is built by reversing the holes and computing an internal
    // skeleton. Hence, the result also needs to be reversed.
    for(std::shared_ptr<Polygon_2> ptr : raw_output)
      ptr->reverse_orientation();

    Offset_polygons_with_holes output = CGAL::arrange_offset_polygons_2<Polygon_with_holes_2>(raw_output);
    construct_horizontal_faces(output, height, points, faces);

    // if the height is negative (extruding downwards), we need to invert all the faces
    if(is_negative(height))
    {
      for(auto& f : faces)
        std::swap(f[0], f[1]);
    }

#ifdef CGAL_SLS_SNAP_TO_VERTICAL_SLABS
    apply_snapping<Geom_traits>(points, snapped_positions);
#endif

    return true;
  }
};

////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////

// convert angles (in degrees) to weights, and handle vertical angles
template <typename FT, typename AngleRange>
void convert_angles(AngleRange& angles)
{
  CGAL_precondition(!angles.empty());

  auto angle_to_weight = [](const FT& angle) -> FT
  {
    CGAL_precondition(0 < angle && angle < 180);

    // @todo should this be an epsilon around 90°? As theta goes to 90°, tan(theta) goes to infinity
    // and thus we could get numerical issues (overflows) if the kernel is not exact
    if(angle == 90)
      return 0;
    else
      return std::tan(CGAL::to_double(angle * CGAL_PI / 180));
  };

  for(auto& contour_angles : angles)
    for(FT& angle : contour_angles)
      angle = angle_to_weight(angle);
}

// handle vertical angles (inf speed)
// returns
// - whether the weights are positive or negative (inward / outward)
// - whether the input weights are valid
// - the weight of vertical slabs
template <typename FT, typename WeightRange>
std::tuple<Slope, bool, FT>
preprocess_weights(WeightRange& weights)
{
  CGAL_precondition(!weights.empty());

  Slope slope = Slope::UNKNOWN;

  FT max_value = 0; // non-inf, maximum absolute value
  for(auto& contour_weights : weights)
  {
    for(FT& w : contour_weights)
    {
      // '0' means a vertical slab, aka 90° angle (see preprocess_angles())
      if(w == 0)
        continue;

      // determine whether weights indicate all inward or all outward
      if(slope == Slope::UNKNOWN)
      {
        // w is neither 0 nor inf here
        slope = (w > 0) ? Slope::INWARD : Slope::OUTWARD;
      }
      else if(slope == Slope::INWARD && w < 0)
      {
        std::cerr << "Error: mixing positive and negative weights is not yet supported" << std::endl;
        return {Slope::UNKNOWN, false, FT(-1)};
      }
      else if(slope == Slope::OUTWARD && w > 0)
      {
        std::cerr << "Error: mixing positive and negative weights is not yet supported" << std::endl;
        return {Slope::UNKNOWN, false, FT(-1)};
      }

      // if we are going outwards, it is just an interior skeleton with opposite weights
      w = CGAL::abs(w);
      if(w > max_value)
        max_value = w;
    }
  }

  if(slope == Slope::UNKNOWN)
  {
    std::cerr << "Warning: all edges vertical?" << std::endl;
    slope = Slope::VERTICAL;
  }

  // Take a weight which is a large % of the max value to ensure there's no ambiguity
  //
  // Since the max value might not be very close to 90°, take the max between of the large-% weight
  // and the weight corresponding to an angle of 89.9999999°
  const FT weight_of_89d9999999 = 572957787.3425436; // tan(89.9999999°)
  const FT scaled_max = (std::max)(weight_of_89d9999999, FT(1e3) * max_value);

  for(auto& contour_weights : weights)
  {
    for(FT& w : contour_weights)
    {
      if(w == FT(0))
        w = scaled_max;
    }
  }

  return {slope, true, scaled_max};
}

template <typename PolygonWithHoles,
          typename WeightRange,
          typename PolygonMesh,
          typename NamedParameters>
bool extrude_skeleton(const PolygonWithHoles& pwh,
                      WeightRange& weights,
                      PolygonMesh& out,
                      const NamedParameters& np)
{
  namespace PMP = ::CGAL::Polygon_mesh_processing;

  using Polygon_2 = typename PolygonWithHoles::Polygon_2;
  using Point = typename boost::range_value<Polygon_2>::type;

  using Default_kernel = typename Kernel_traits<Point>::type;
  using Geom_traits = typename internal_np::Lookup_named_param_def<internal_np::geom_traits_t,
                                                                   NamedParameters,
                                                                   Default_kernel>::type;
  using FT = typename Geom_traits::FT;

  using Point_3 = typename Geom_traits::Point_3;

  using parameters::choose_parameter;
  using parameters::get_parameter;

  const bool verbose = choose_parameter(get_parameter(np, internal_np::verbose), false);
  Geom_traits gt = choose_parameter<Geom_traits>(get_parameter(np, CGAL::internal_np::geom_traits));

  const FT height = choose_parameter(get_parameter(np, internal_np::maximum_height), default_extrusion_height<FT>());

  Slope slope;
  bool valid_input;
  FT vertical_weight;
  std::tie(slope, valid_input, vertical_weight) = preprocess_weights<FT>(weights);

  if(!valid_input)
  {
    if(verbose)
      std::cerr << "Error: invalid input weights" << std::endl;
    return false;
  }

  if(verbose)
  {
    switch(slope)
    {
      case Slope::UNKNOWN: std::cout << "Slope is UNKNOWN??" << std::endl; break;
      case Slope::INWARD: std::cout << "Slope is INWARD" << std::endl; break;
      case Slope::OUTWARD: std::cout << "Slope is OUTWARD" << std::endl; break;
      case Slope::VERTICAL: std::cout << "Slope is VERTICAL" << std::endl; break;
    }
  }

  // End of preprocessing, start the actual skeleton computation

  if(slope != Slope::INWARD && height == default_extrusion_height<FT>())
  {
    if(verbose)
      std::cerr << "Error: height must be specified when using an outward (or vertical) slope" << std::endl;
    return false;
  }

  // build a soup, to be converted to a mesh afterwards
  std::vector<Point_3> points;
  std::vector<std::vector<std::size_t> > faces;
  points.reserve(2 * pwh.outer_boundary().size()); // just a reasonable guess
  faces.reserve(2 * pwh.outer_boundary().size() + 2*pwh.number_of_holes());

  Extrusion_builder<Geom_traits> builder(gt);
  bool res;
  if(slope != Slope::OUTWARD) // INWARD or VERTICAL
    res = builder.inward_construction(pwh, weights, vertical_weight, height, points, faces);
  else
    res = builder.outward_construction(pwh, weights, vertical_weight, height, points, faces);

  if(!res)
    return false;

#ifdef CGAL_SLS_OUTPUT_FILES
  // This soup provides one connected component per edge of the input polygon
  CGAL::IO::write_polygon_soup("extruded_skeleton_soup.off", points, faces, CGAL::parameters::stream_precision(17));
#endif

  // Convert the triangle soup to a triangle mesh

  PMP::merge_duplicate_points_in_polygon_soup(points, faces);
  if(!PMP::is_polygon_soup_a_polygon_mesh(faces))
    PMP::orient_polygon_soup(points, faces);
  CGAL_assertion(PMP::is_polygon_soup_a_polygon_mesh(faces));

  PMP::polygon_soup_to_polygon_mesh(points, faces, out);

  CGAL_warning(is_valid_polygon_mesh(out) && is_closed(out));

  return true;
}

} // namespace internal
} // namespace Straight_skeleton_extrusion

// Documented in the Straight_skeleton_2 package
template <typename PolygonWithHoles,
          typename PolygonMesh,
          typename NamedParameters = parameters::Default_named_parameters>
bool extrude_skeleton(const PolygonWithHoles& pwh,
                      PolygonMesh& out,
                      const NamedParameters& np = parameters::default_values(),
                      std::enable_if_t<CGAL_SS_i::has_Hole_const_iterator<PolygonWithHoles>::value>* = nullptr)
{
  namespace SSEI = Straight_skeleton_extrusion::internal;

  using Polygon_2 = typename PolygonWithHoles::General_polygon_2;
  using K = typename Kernel_traits<typename boost::range_value<Polygon_2>::type>::type;
  using FT = typename K::FT;
  using Default_speed_type = std::vector<std::vector<FT> >;

  using parameters::choose_parameter;
  using parameters::is_default_parameter;
  using parameters::get_parameter_reference;

  const bool has_weights = !(is_default_parameter<NamedParameters, internal_np::weights_param_t>::value);
  const bool has_angles = !(is_default_parameter<NamedParameters, internal_np::angles_param_t>::value);

  Default_speed_type def_speeds; // never used, but needed for compilation

  if(has_weights)
  {
    // not using "Angles" here is on purpose, I want to copy the range but the NP is ref only
    auto weights = choose_parameter(get_parameter_reference(np, internal_np::weights_param), def_speeds);
    return SSEI::extrude_skeleton(pwh, weights, out, np);
  }
  else if(has_angles)
  {
    // not using "Weights" here is on purpose, I want to copy the range but the NP is ref only
    auto angles = choose_parameter(get_parameter_reference(np, internal_np::angles_param), def_speeds);
    SSEI::convert_angles<FT>(angles);
    return SSEI::extrude_skeleton(pwh, angles, out, np);
  }
  else // neither angles nor weights were provided
  {
    std::vector<std::vector<FT> > uniform_weights;
    uniform_weights.reserve(pwh.number_of_holes() + 1);

    uniform_weights.push_back(std::vector<FT>(pwh.outer_boundary().size(), FT(1)));
    for(const auto& hole : pwh.holes())
      uniform_weights.push_back(std::vector<FT>(hole.size(), FT(1)));

    return SSEI::extrude_skeleton(pwh, uniform_weights, out, np);
  }
}

////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////

template <typename Polygon, // without holes
          typename PolygonMesh,
          typename NamedParameters = parameters::Default_named_parameters>
bool extrude_skeleton(const Polygon& p,
                      PolygonMesh& out,
                      const NamedParameters& np = parameters::default_values(),
                      std::enable_if_t<!CGAL_SS_i::has_Hole_const_iterator<Polygon>::value>* = nullptr)
{
  using Polygon_with_holes = CGAL::General_polygon_with_holes_2<Polygon>;

  return extrude_skeleton(Polygon_with_holes(p), out, np);
}

} // namespace CGAL

#endif // CGAL_SLS_EXTRUSION_EXTRUDE_SKELETON_H
