#pragma once
// layout.hpp
//
// Phase 5/6/7 — Layout / embedding: DOF vector → vertex coordinates in the
// target geometry via BFS-trilateration.
//
// ┌──────────────────────────────────────────────────────────────────────────┐
// │  Algorithm (all three geometries)                                        │
// │                                                                          │
// │  1. For every edge e compute the updated length l_e from the DOF x.     │
// │  2. Choose root face = largest 3-D area face (quality heuristic).       │
// │  3. Priority BFS over the dual graph: faces are processed in order of   │
// │     shortest distance (BFS depth) from the root face, minimising error  │
// │     accumulation.  Equal depth: arbitrary tie-break.                    │
// │  4. For each face: trilaterate the one unplaced vertex from the two     │
// │     already-placed vertices on the shared edge.                          │
// │  5. If a CutGraph is supplied, cut edges are treated as boundary;       │
// │     holonomy is recorded for each cut edge.                              │
// │  6. After the first connected component, unvisited faces start new      │
// │     BFS sweeps (multi-component support).                               │
// │                                                                          │
// │  For open meshes: globally consistent placement.                        │
// │  For closed meshes without CutGraph: first (shallowest) visit wins,    │
// │     has_seam = true.                                                    │
// │  For closed meshes with CutGraph: each vertex placed exactly once;     │
// │     holonomy = translation (Euclidean) or Möbius map (hyperbolic).     │
// └──────────────────────────────────────────────────────────────────────────┘
//
// Trilateration
//   Euclidean   — exact (analytic formula in ℝ²)
//   Spherical   — exact (spherical law of cosines on S²)
//   Hyperbolic  — exact (hyperbolic law of cosines + Möbius maps,
//                 Poincaré disk model)
//
// Normalisation
//   normalise_euclidean(layout)   — centroid → origin, major axis → x-axis (PCA)
//   normalise_hyperbolic(layout)  — face-area-weighted iterative Möbius centering
//   normalise_spherical(layout)   — rotate centroid to north pole (Rodrigues)
//
// Holonomy (genus-g surfaces with CutGraph)
//   HolonomyData.translations[i]  — translation ω_i   (Euclidean / spherical)
//   HolonomyData.mobius_maps[i]   — Möbius map T_i    (hyperbolic)
//   For a flat torus: ω_1, ω_2 are the lattice generators; τ = ω_2/ω_1 ∈ ℍ.
//
// Texture atlas
//   Layout2D.halfedge_uv[h.idx()] — UV of source(h) as seen from face(h).
//   At seam edges the two opposite halfedges carry different UV values,
//   enabling proper per-halfedge UV for GPU texture atlasing.
//
// Möbius map
//   MobiusMap::from_three(z1→w1, z2→w2, z3→w3) — fit a Möbius transformation
//   to three point correspondences (via 3×3 complex linear system).

#include "conformal_mesh.hpp"
#include "euclidean_functional.hpp"
#include "spherical_functional.hpp"
#include "hyper_ideal_functional.hpp"
#include "cut_graph.hpp"
#include <Eigen/Dense>
#include <vector>
#include <queue>
#include <complex>
#include <cmath>
#include <algorithm>
#include <limits>
#include <fstream>
#include <string>

namespace conformallab {

// ── Möbius map ────────────────────────────────────────────────────────────────
//
// T(z) = (a·z + b) / (c·z + d)
//
// For hyperbolic holonomy the map is an orientation-preserving isometry of
// the Poincaré disk (SU(1,1) element).
// ─────────────────────────────────────────────────────────────────────────────
struct MobiusMap {
    using C = std::complex<double>;
    C a{1.0, 0.0};
    C b{0.0, 0.0};
    C c{0.0, 0.0};
    C d{1.0, 0.0};

    C apply(C z) const { return (a * z + b) / (c * z + d); }

    Eigen::Vector2d apply(const Eigen::Vector2d& p) const {
        C w = apply(C(p.x(), p.y()));
        return Eigen::Vector2d(w.real(), w.imag());
    }

    static MobiusMap identity() { return {C(1), C(0), C(0), C(1)}; }

    MobiusMap inverse()               const { return {d, -b, -c, a}; }
    MobiusMap compose(const MobiusMap& T) const {
        return { a*T.a + b*T.c,  a*T.b + b*T.d,
                 c*T.a + d*T.c,  c*T.b + d*T.d };
    }

    bool is_identity(double tol = 1e-9) const {
        if (std::abs(d) < 1e-14) return false;
        C a_ = a/d, b_ = b/d, c_ = c/d;
        return std::abs(a_ - C(1)) < tol && std::abs(b_) < tol && std::abs(c_) < tol;
    }

    /// Fit T to three point correspondences  T(z_i) = w_i.
    /// Sets d=1 and solves the 3×3 complex linear system.
    /// Returns identity when the system is (near-)singular.
    static MobiusMap from_three(C z1, C w1, C z2, C w2, C z3, C w3) {
        Eigen::Matrix3cd M;
        M << z1, C(1), -w1*z1,
             z2, C(1), -w2*z2,
             z3, C(1), -w3*z3;
        Eigen::Vector3cd rhs; rhs << w1, w2, w3;
        Eigen::ColPivHouseholderQR<Eigen::Matrix3cd> qr(M);
        if (qr.rank() < 3) return identity();
        Eigen::Vector3cd sol = qr.solve(rhs);
        return { sol[0], sol[1], sol[2], C(1.0) };
    }
};

// ── Result types ──────────────────────────────────────────────────────────────

struct Layout2D {
    /// uv[v.idx()] — primary 2-D position (first / shallowest-BFS-depth visit).
    std::vector<Eigen::Vector2d> uv;

    /// halfedge_uv[h.idx()] — UV of source(h) as seen from face(h).
    ///
    /// For interior (non-seam) halfedges: equals uv[source(h).idx()].
    /// For seam halfedges:  carries the UV from the virtual unfolding across
    ///   the cut (i.e. the trilaterated position that was NOT used as the
    ///   primary uv).  This gives each face its own copy of a seam vertex,
    ///   enabling a proper GPU texture atlas without vertex duplication.
    ///
    /// Size = mesh.number_of_halfedges().  Border halfedges = (0,0).
    std::vector<Eigen::Vector2d> halfedge_uv;

    bool success  = false;
    bool has_seam = false;   ///< true when a vertex was reached via two paths
};

struct Layout3D {
    std::vector<Eigen::Vector3d> pos;
    bool success  = false;
    bool has_seam = false;
};

/// Per-cut-edge holonomy.
///
/// Euclidean: translations[i] = ω_i  (translation vector for cut_edge_indices[i]).
///   For a flat torus ω_1, ω_2 are the lattice generators; τ = ω_2/ω_1 ∈ ℍ.
///
/// Hyperbolic: mobius_maps[i] = T_i  (Möbius isometry of the Poincaré disk).
///   T_i(p_actual) = p_virtual  — maps the placed vertex position to the
///   trilaterated virtual position obtained by continuing the unfolding across
///   the cut.
struct HolonomyData {
    std::vector<Eigen::Vector2d> translations;   ///< Euclidean / spherical
    std::vector<MobiusMap>       mobius_maps;     ///< hyperbolic (Phase 7)
    std::vector<std::size_t>     cut_edge_indices;
};

// ── Internal helpers ──────────────────────────────────────────────────────────

namespace detail {

// ─────────────────────────────────────────────────────────────────────────────
// Priority-BFS queue entry
// Faces closer to the root (lower depth) are processed first, reducing the
// accumulation of trilateration errors for later-placed vertices.
// ─────────────────────────────────────────────────────────────────────────────
struct BFSEntry {
    int            depth;  ///< BFS depth = max(depth[v_src], depth[v_tgt]) + 1
    Halfedge_index h;      ///< halfedge pointing INTO the face to be placed
    /// min-heap: smaller depth = higher priority
    bool operator>(const BFSEntry& o) const { return depth > o.depth; }
};

using BFSQueue = std::priority_queue<BFSEntry,
                                     std::vector<BFSEntry>,
                                     std::greater<BFSEntry>>;

// ─────────────────────────────────────────────────────────────────────────────
// Euclidean trilateration — LEFT (CCW) side of p_a → p_b
// ─────────────────────────────────────────────────────────────────────────────
inline Eigen::Vector2d trilaterate_2d(
    const Eigen::Vector2d& pa, const Eigen::Vector2d& pb,
    double da, double db)
{
    Eigen::Vector2d ab = pb - pa;
    double d = ab.norm();
    if (d < 1e-14) return pa;
    Eigen::Vector2d e1 = ab / d;
    Eigen::Vector2d e2(-e1.y(), e1.x());
    double t  = (da*da - db*db + d*d) / (2.0 * d);
    double s2 = da*da - t*t;
    double s  = (s2 > 0.0) ? std::sqrt(s2) : 0.0;
    return pa + t*e1 + s*e2;
}

// ─────────────────────────────────────────────────────────────────────────────
// Spherical trilateration (S²) — LEFT side of geodesic arc pa → pb
// ─────────────────────────────────────────────────────────────────────────────
inline Eigen::Vector3d trilaterate_sph(
    const Eigen::Vector3d& pa, const Eigen::Vector3d& pb,
    double da, double db)
{
    double c     = pa.dot(pb);
    double denom = 1.0 - c*c;
    if (denom < 1e-14) return pa;
    double cda = std::cos(da), cdb = std::cos(db);
    double alpha = (cda - c*cdb) / denom;
    double beta  = (cdb - c*cda) / denom;
    Eigen::Vector3d cross  = pa.cross(pb);
    double          cn2    = cross.squaredNorm();
    double gamma2 = 1.0 - alpha*alpha - beta*beta - 2.0*alpha*beta*c;
    double gamma  = (gamma2 > 0.0 && cn2 > 1e-28) ? std::sqrt(gamma2 / cn2) : 0.0;
    Eigen::Vector3d p = alpha*pa + beta*pb + gamma*cross;
    double n = p.norm();
    return (n > 1e-14) ? (p / n) : pa;
}

// ─────────────────────────────────────────────────────────────────────────────
// Hyperbolic trilateration — exact via Möbius + hyperbolic law of cosines.
// LEFT (CCW) side of pa → pb in the Poincaré disk.
// ─────────────────────────────────────────────────────────────────────────────
inline Eigen::Vector2d trilaterate_hyp(
    const Eigen::Vector2d& pa, const Eigen::Vector2d& pb,
    double D, double da, double db)
{
    using C = std::complex<double>;
    if (D < 1e-12 || da < 1e-12) return pa;
    if (std::sinh(da) * std::sinh(D) < 1e-14) return pa;
    C a(pa.x(), pa.y()), b(pb.x(), pb.y());
    auto fwd = [](C z, C c_) { return (z-c_)/(C(1)-std::conj(c_)*z); };
    auto inv = [](C w, C c_) { return (w+c_)/(C(1)+std::conj(c_)*w); };
    double theta_b  = std::arg(fwd(b, a));
    double cos_alpha = (std::cosh(da)*std::cosh(D) - std::cosh(db))
                     / (std::sinh(da)*std::sinh(D));
    cos_alpha = std::max(-1.0, std::min(1.0, cos_alpha));
    C pc_origin = std::tanh(da*0.5) * std::exp(C(0.0, theta_b + std::acos(cos_alpha)));
    C pc_c = inv(pc_origin, a);
    return Eigen::Vector2d(pc_c.real(), pc_c.imag());
}

// ─────────────────────────────────────────────────────────────────────────────
// 3-D face area (cross product / 2)
// ─────────────────────────────────────────────────────────────────────────────
inline double face_3d_area(const ConformalMesh& mesh, Face_index f)
{
    Halfedge_index h = mesh.halfedge(f);
    auto pA = mesh.point(mesh.source(h));
    auto pB = mesh.point(mesh.target(h));
    auto pC = mesh.point(mesh.target(mesh.next(h)));
    Eigen::Vector3d ab(pB.x()-pA.x(), pB.y()-pA.y(), pB.z()-pA.z());
    Eigen::Vector3d ac(pC.x()-pA.x(), pC.y()-pA.y(), pC.z()-pA.z());
    return 0.5 * ab.cross(ac).norm();
}

// ─────────────────────────────────────────────────────────────────────────────
// Best root face: largest 3-D area, with 1.5× bonus for interior faces.
// ─────────────────────────────────────────────────────────────────────────────
inline Face_index best_root_face(const ConformalMesh& mesh)
{
    Face_index best;
    double best_score = -1.0;
    for (auto f : mesh.faces()) {
        double area = face_3d_area(mesh, f);
        bool interior = true;
        for (auto h : CGAL::halfedges_around_face(mesh.halfedge(f), mesh))
            if (mesh.is_border(mesh.opposite(h))) { interior = false; break; }
        double score = area * (interior ? 1.5 : 1.0);
        if (score > best_score) { best_score = score; best = f; }
    }
    return best;
}

// ─────────────────────────────────────────────────────────────────────────────
// Euclidean bounding box of placed vertices (for multi-component offset)
// ─────────────────────────────────────────────────────────────────────────────
inline void bbox_2d(const std::vector<Eigen::Vector2d>& uv,
                    const std::vector<bool>& placed,
                    double& xmax)
{
    xmax = 0.0;
    for (std::size_t i = 0; i < uv.size(); ++i)
        if (placed[i]) xmax = std::max(xmax, uv[i].x());
}

// ─────────────────────────────────────────────────────────────────────────────
// Möbius centering — unweighted (fallback / Phase 6 compat.)
// ─────────────────────────────────────────────────────────────────────────────
inline void center_poincare_disk(std::vector<Eigen::Vector2d>& uv)
{
    if (uv.empty()) return;
    using C = std::complex<double>;
    C c(0);
    for (auto& p : uv) c += C(p.x(), p.y());
    c /= static_cast<double>(uv.size());
    double r = std::abs(c);
    if (r > 0.999) c *= 0.999/r;
    if (r < 1e-12) return;
    for (auto& p : uv) {
        C z(p.x(), p.y());
        C w = (z-c)/(C(1)-std::conj(c)*z);
        p = {w.real(), w.imag()};
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// Face-area-weighted iterative Möbius centering (Fréchet mean, Phase 7).
// weights[i] = Voronoi area of vertex i.
// ─────────────────────────────────────────────────────────────────────────────
inline void center_poincare_disk_weighted(
    std::vector<Eigen::Vector2d>& uv, const std::vector<double>& weights)
{
    if (uv.empty()) return;
    using C = std::complex<double>;
    double total_w = 0.0; for (double w : weights) total_w += w;
    if (total_w < 1e-14) { center_poincare_disk(uv); return; }
    C c(0);
    for (int iter = 0; iter < 30; ++iter) {
        C mt(0);
        for (std::size_t i = 0; i < uv.size(); ++i) {
            C z(uv[i].x(), uv[i].y());
            mt += weights[i] * (z-c)/(C(1)-std::conj(c)*z);
        }
        mt /= total_w;
        C c_new = (mt+c)/(C(1)+std::conj(c)*mt);
        if (std::abs(c_new-c) < 1e-12) { c = c_new; break; }
        c = c_new;
    }
    double r = std::abs(c);
    if (r > 0.999) c *= 0.999/r;
    if (r < 1e-12) return;
    for (auto& p : uv) {
        C z(p.x(), p.y());
        C w = (z-c)/(C(1)-std::conj(c)*z);
        p = {w.real(), w.imag()};
    }
}

} // namespace detail

// ── Vertex Voronoi area weights ───────────────────────────────────────────────
inline std::vector<double> compute_vertex_area_weights(const ConformalMesh& mesh)
{
    std::vector<double> w(mesh.number_of_vertices(), 0.0);
    for (auto f : mesh.faces()) {
        double area = detail::face_3d_area(mesh, f);
        for (auto h : CGAL::halfedges_around_face(mesh.halfedge(f), mesh))
            w[static_cast<std::size_t>(mesh.target(h).idx())] += area / 3.0;
    }
    return w;
}

// ── Layout normalisation ──────────────────────────────────────────────────────

inline void normalise_euclidean(Layout2D& layout)
{
    if (!layout.success || layout.uv.empty()) return;
    const std::size_t n = layout.uv.size();
    Eigen::Vector2d mean = Eigen::Vector2d::Zero();
    for (auto& p : layout.uv) mean += p;
    mean /= static_cast<double>(n);
    for (auto& p : layout.uv) p -= mean;
    for (auto& p : layout.halfedge_uv) p -= mean;
    Eigen::Matrix2d cov = Eigen::Matrix2d::Zero();
    for (auto& p : layout.uv) cov += p * p.transpose();
    cov /= static_cast<double>(n);
    Eigen::SelfAdjointEigenSolver<Eigen::Matrix2d> eig(cov);
    Eigen::Vector2d major = eig.eigenvectors().col(1);
    double angle = -std::atan2(major.y(), major.x());
    Eigen::Matrix2d R;
    R << std::cos(angle), -std::sin(angle),
         std::sin(angle),  std::cos(angle);
    for (auto& p : layout.uv)         p = R * p;
    for (auto& p : layout.halfedge_uv) p = R * p;
}

/// Hyperbolic — face-area-weighted iterative Möbius centering.
inline void normalise_hyperbolic(Layout2D& layout, const ConformalMesh& mesh)
{
    if (!layout.success || layout.uv.empty()) return;
    auto weights = compute_vertex_area_weights(mesh);
    detail::center_poincare_disk_weighted(layout.uv, weights);
    // halfedge_uv follows the same Möbius map
    detail::center_poincare_disk(layout.halfedge_uv);
}
inline void normalise_hyperbolic(Layout2D& layout) // fallback without mesh
{
    if (!layout.success || layout.uv.empty()) return;
    detail::center_poincare_disk(layout.uv);
    detail::center_poincare_disk(layout.halfedge_uv);
}

inline void normalise_spherical(Layout3D& layout)
{
    if (!layout.success || layout.pos.empty()) return;
    Eigen::Vector3d mean = Eigen::Vector3d::Zero();
    for (auto& p : layout.pos) mean += p;
    double len = mean.norm(); if (len < 1e-12) return;
    mean /= len;
    Eigen::Vector3d north(0, 0, 1);
    Eigen::Vector3d axis = mean.cross(north);
    double sin_a = axis.norm(), cos_a = mean.dot(north);
    if (sin_a < 1e-12) return;
    axis /= sin_a;
    Eigen::Matrix3d K;
    K <<    0, -axis.z(),  axis.y(),
       axis.z(),       0, -axis.x(),
      -axis.y(),  axis.x(),       0;
    Eigen::Matrix3d Rot = Eigen::Matrix3d::Identity() + sin_a*K + (1-cos_a)*K*K;
    for (auto& p : layout.pos) p = Rot * p;
}

// ─────────────────────────────────────────────────────────────────────────────
// BFS placement helpers shared across all three layout functions.
//
// set_face_huv: populate halfedge_uv for the three halfedges of face f,
//               given that the face was entered via halfedge h_enter.
//   h_enter: source=v_src, target=v_tgt   → huv = uv[v_src]
//   next(h_enter): source=v_tgt, target=v_new → huv = uv[v_tgt]
//   prev(h_enter): source=v_new, target=v_src → huv = p_new  (trilaterated)
// ─────────────────────────────────────────────────────────────────────────────
namespace detail {

inline void set_face_huv_2d(
    std::vector<Eigen::Vector2d>& huv,
    const ConformalMesh& mesh,
    Halfedge_index h,
    const std::vector<Eigen::Vector2d>& uv,
    const Eigen::Vector2d& p_new)
{
    auto s = [](std::size_t x){ return x; };
    huv[s(static_cast<std::size_t>(h.idx()))]               = uv[static_cast<std::size_t>(mesh.source(h).idx())];
    huv[s(static_cast<std::size_t>(mesh.next(h).idx()))]    = uv[static_cast<std::size_t>(mesh.target(h).idx())];
    huv[s(static_cast<std::size_t>(mesh.prev(h).idx()))]    = p_new;
}

inline void set_root_huv_2d(
    std::vector<Eigen::Vector2d>& huv,
    const ConformalMesh& mesh,
    Face_index f,
    const std::vector<Eigen::Vector2d>& uv)
{
    for (auto hf : CGAL::halfedges_around_face(mesh.halfedge(f), mesh))
        huv[static_cast<std::size_t>(hf.idx())] = uv[static_cast<std::size_t>(mesh.source(hf).idx())];
}

} // namespace detail

// ── Euclidean layout ──────────────────────────────────────────────────────────

/// Embed the mesh in ℝ² using the Euclidean metric encoded in x.
///
/// Runs priority-BFS trilateration: places vertices in order of BFS depth
/// from the root face (largest 3D area), so errors accumulate last.
/// For closed genus-g surfaces a CutGraph must be supplied — otherwise
/// the layout will have a seam discontinuity (Layout2D::has_seam = true).
///
/// \param mesh      Input surface mesh.  Must have lambda0 and v_idx set in maps.
/// \param x         DOF vector returned by newton_euclidean().
/// \param maps      EuclideanMaps (lambda0, v_idx, e_idx).
/// \param cut       Optional cut graph (compute_cut_graph()).  Pass nullptr for
///                  open meshes or if seams are acceptable.
/// \param holonomy  If non-null and cut != nullptr, receives the lattice
///                  translations ω_i ∈ ℂ per cut edge.
///                  Pass to compute_period_matrix() for the conformal modulus τ.
/// \param normalise If true, calls normalise_euclidean() on the result:
///                  centroid → origin, major axis → x-axis (PCA).
/// \return          Layout2D with .uv[v] (per-vertex UV) and
///                  .halfedge_uv[h] (per-halfedge UV for texture atlasing).
///
/// \note halfedge_uv differs from uv at seam edges: the two sides of a cut
///       carry different UV coordinates for proper GPU texture atlasing.
inline Layout2D euclidean_layout(
    ConformalMesh&             mesh,
    const std::vector<double>& x,
    const EuclideanMaps&       maps,
    const CutGraph*            cut      = nullptr,
    HolonomyData*              holonomy  = nullptr,
    bool                       normalise = false)
{
    const std::size_t nv = mesh.number_of_vertices();
    const std::size_t nf = mesh.number_of_faces();
    const std::size_t nh = mesh.number_of_halfedges();
    Layout2D result;
    result.uv.assign(nv, Eigen::Vector2d::Zero());
    result.halfedge_uv.assign(nh, Eigen::Vector2d::Zero());
    if (nf == 0) return result;

    auto get_u  = [&](Vertex_index v) {
        int iv = maps.v_idx[v]; return (iv>=0) ? x[static_cast<std::size_t>(iv)] : 0.0; };
    auto get_ue = [&](Edge_index e) {
        int ie = maps.e_idx[e]; return (ie>=0) ? x[static_cast<std::size_t>(ie)] : 0.0; };
    auto edge_len = [&](Halfedge_index h) {
        Edge_index e = mesh.edge(h);
        double lam = maps.lambda0[e] + get_u(mesh.source(h)) + get_u(mesh.target(h)) + get_ue(e);
        return std::exp(lam * 0.5); };

    std::vector<bool> vertex_placed(nv, false);
    std::vector<bool> face_placed(nf, false);
    std::vector<int>  vertex_depth(nv, std::numeric_limits<int>::max());

    auto place_component = [&](Face_index f_root, Eigen::Vector2d offset) {
        Halfedge_index h0 = mesh.halfedge(f_root);
        Halfedge_index h1 = mesh.next(h0), h2 = mesh.next(h1);
        Vertex_index vA = mesh.source(h0), vB = mesh.source(h1), vC = mesh.source(h2);
        double lAB = edge_len(h0), lBC = edge_len(h1), lCA = edge_len(h2);
        result.uv[vA.idx()] = offset;
        result.uv[vB.idx()] = offset + Eigen::Vector2d(lAB, 0.0);
        result.uv[vC.idx()] = detail::trilaterate_2d(result.uv[vA.idx()], result.uv[vB.idx()], lCA, lBC);
        vertex_placed[vA.idx()] = vertex_placed[vB.idx()] = vertex_placed[vC.idx()] = true;
        vertex_depth[vA.idx()]  = vertex_depth[vB.idx()]  = vertex_depth[vC.idx()]  = 0;
        face_placed[f_root.idx()] = true;
        detail::set_root_huv_2d(result.halfedge_uv, mesh, f_root, result.uv);

        detail::BFSQueue pq;
        auto enqueue = [&](Face_index f) {
            for (auto hf : CGAL::halfedges_around_face(mesh.halfedge(f), mesh)) {
                Halfedge_index ho = mesh.opposite(hf);
                if (mesh.is_border(ho)) continue;
                if (cut && cut->is_cut(mesh.edge(hf))) continue;
                Face_index fadj = mesh.face(ho);
                if (!face_placed[fadj.idx()]) {
                    int d = std::max(vertex_depth[mesh.source(ho).idx()],
                                     vertex_depth[mesh.target(ho).idx()]) + 1;
                    pq.push({d, ho});
                }
            }
        };
        enqueue(f_root);

        while (!pq.empty()) {
            auto [depth, h] = pq.top(); pq.pop();
            Face_index f = mesh.face(h);
            if (face_placed[f.idx()]) continue;

            Vertex_index v_src = mesh.source(h), v_tgt = mesh.target(h);
            Vertex_index v_new = mesh.target(mesh.next(h));
            Eigen::Vector2d p = detail::trilaterate_2d(
                result.uv[v_src.idx()], result.uv[v_tgt.idx()],
                edge_len(mesh.prev(h)), edge_len(mesh.next(h)));

            if (!vertex_placed[v_new.idx()]) {
                result.uv[v_new.idx()]     = p;
                vertex_placed[v_new.idx()] = true;
                vertex_depth[v_new.idx()]  = depth;
            } else {
                result.has_seam = true;
            }
            // halfedge_uv: p is the UV of v_new in THIS face (may differ from uv[v_new] at seam)
            detail::set_face_huv_2d(result.halfedge_uv, mesh, h, result.uv, p);
            face_placed[f.idx()] = true;
            enqueue(f);
        }
    };

    place_component(detail::best_root_face(mesh), Eigen::Vector2d::Zero());
    for (auto f : mesh.faces()) {
        if (face_placed[f.idx()]) continue;
        double xmax; detail::bbox_2d(result.uv, vertex_placed, xmax);
        place_component(f, Eigen::Vector2d(xmax * 1.1 + 1.0, 0.0));
    }
    result.success = true;

    // ── Holonomy ──────────────────────────────────────────────────────────────
    if (cut && holonomy) {
        holonomy->cut_edge_indices = cut->cut_edge_indices;
        holonomy->translations.clear();
        holonomy->translations.reserve(cut->cut_edge_indices.size());
        holonomy->mobius_maps.clear();
        for (std::size_t ce_idx : cut->cut_edge_indices) {
            Edge_index e = *std::next(mesh.edges().begin(), static_cast<std::ptrdiff_t>(ce_idx));
            Halfedge_index h  = mesh.halfedge(e);
            Halfedge_index ho = mesh.opposite(h);
            Halfedge_index hx = mesh.is_border(ho) ? h : ho;
            if (mesh.is_border(hx)) { holonomy->translations.push_back(Eigen::Vector2d::Zero()); continue; }
            Vertex_index vs = mesh.source(hx), vt = mesh.target(hx), vn = mesh.target(mesh.next(hx));
            if (!vertex_placed[vn.idx()]) { holonomy->translations.push_back(Eigen::Vector2d::Zero()); continue; }
            Eigen::Vector2d p_tri = detail::trilaterate_2d(
                result.uv[vs.idx()], result.uv[vt.idx()],
                edge_len(mesh.prev(hx)), edge_len(mesh.next(hx)));
            // Store seam UV for the cut-crossing halfedges
            detail::set_face_huv_2d(result.halfedge_uv, mesh, hx, result.uv, p_tri);
            holonomy->translations.push_back(p_tri - result.uv[vn.idx()]);
        }
    }

    if (normalise) normalise_euclidean(result);
    return result;
}

// ── Spherical layout ──────────────────────────────────────────────────────────

/// Embed the mesh on the unit sphere S² using the spherical metric encoded in x.
///
/// Runs priority-BFS trilateration using the spherical law of cosines.
/// Typical use: genus-0 (sphere-like) surfaces after newton_spherical().
///
/// \param mesh      Input genus-0 surface mesh.
/// \param x         DOF vector returned by newton_spherical().
/// \param maps      SphericalMaps.
/// \param cut       Optional cut graph (rarely needed for genus-0).
/// \param holonomy  If non-null, receives rotational holonomies (spherical).
/// \param normalise If true, calls normalise_spherical(): rotates the centroid
///                  to the north pole (Rodrigues rotation formula).
/// \return          Layout3D with .xyz[v] ∈ S² ⊂ ℝ³ for each vertex.
inline Layout3D spherical_layout(
    ConformalMesh&             mesh,
    const std::vector<double>& x,
    const SphericalMaps&       maps,
    const CutGraph*            cut      = nullptr,
    HolonomyData*              holonomy  = nullptr,
    bool                       normalise = false)
{
    const std::size_t nv = mesh.number_of_vertices();
    const std::size_t nf = mesh.number_of_faces();
    Layout3D result;
    result.pos.assign(nv, Eigen::Vector3d::Zero());
    if (nf == 0) return result;

    auto get_u  = [&](Vertex_index v) {
        int iv = maps.v_idx[v]; return (iv>=0) ? x[static_cast<std::size_t>(iv)] : 0.0; };
    auto get_ue = [&](Edge_index e) {
        int ie = maps.e_idx[e]; return (ie>=0) ? x[static_cast<std::size_t>(ie)] : 0.0; };
    auto arc_len = [&](Halfedge_index h) {
        Edge_index e = mesh.edge(h);
        double lam = maps.lambda0[e] + get_u(mesh.source(h)) + get_u(mesh.target(h)) + get_ue(e);
        return 2.0 * std::asin(std::min(std::exp(lam*0.5), 1.0)); };

    std::vector<bool> vertex_placed(nv, false);
    std::vector<bool> face_placed(nf, false);
    std::vector<int>  vertex_depth(nv, std::numeric_limits<int>::max());

    auto place_component = [&](Face_index f_root) {
        Halfedge_index h0 = mesh.halfedge(f_root);
        Halfedge_index h1 = mesh.next(h0), h2 = mesh.next(h1);
        Vertex_index vA = mesh.source(h0), vB = mesh.source(h1), vC = mesh.source(h2);
        double lAB = arc_len(h0), lBC = arc_len(h1), lCA = arc_len(h2);
        result.pos[vA.idx()] = Eigen::Vector3d(0, 0, 1);
        result.pos[vB.idx()] = Eigen::Vector3d(std::sin(lAB), 0, std::cos(lAB));
        result.pos[vC.idx()] = detail::trilaterate_sph(result.pos[vA.idx()], result.pos[vB.idx()], lCA, lBC);
        vertex_placed[vA.idx()] = vertex_placed[vB.idx()] = vertex_placed[vC.idx()] = true;
        vertex_depth[vA.idx()]  = vertex_depth[vB.idx()]  = vertex_depth[vC.idx()]  = 0;
        face_placed[f_root.idx()] = true;

        detail::BFSQueue pq;
        auto enqueue = [&](Face_index f) {
            for (auto hf : CGAL::halfedges_around_face(mesh.halfedge(f), mesh)) {
                Halfedge_index ho = mesh.opposite(hf);
                if (mesh.is_border(ho)) continue;
                if (cut && cut->is_cut(mesh.edge(hf))) continue;
                Face_index fadj = mesh.face(ho);
                if (!face_placed[fadj.idx()]) {
                    int d = std::max(vertex_depth[mesh.source(ho).idx()],
                                     vertex_depth[mesh.target(ho).idx()]) + 1;
                    pq.push({d, ho});
                }
            }
        };
        enqueue(f_root);

        while (!pq.empty()) {
            auto [depth, h] = pq.top(); pq.pop();
            Face_index f = mesh.face(h);
            if (face_placed[f.idx()]) continue;
            Vertex_index vs = mesh.source(h), vt = mesh.target(h), vn = mesh.target(mesh.next(h));
            Eigen::Vector3d p = detail::trilaterate_sph(
                result.pos[vs.idx()], result.pos[vt.idx()],
                arc_len(mesh.prev(h)), arc_len(mesh.next(h)));
            if (!vertex_placed[vn.idx()]) {
                result.pos[vn.idx()] = p; vertex_placed[vn.idx()] = true; vertex_depth[vn.idx()] = depth;
            } else result.has_seam = true;
            face_placed[f.idx()] = true; enqueue(f);
        }
    };

    place_component(detail::best_root_face(mesh));
    for (auto f : mesh.faces()) if (!face_placed[f.idx()]) place_component(f);
    result.success = true;

    if (cut && holonomy) {
        holonomy->cut_edge_indices = cut->cut_edge_indices;
        holonomy->translations.clear();
        holonomy->translations.reserve(cut->cut_edge_indices.size());
        holonomy->mobius_maps.clear();
        for (std::size_t ce_idx : cut->cut_edge_indices) {
            Edge_index e = *std::next(mesh.edges().begin(), static_cast<std::ptrdiff_t>(ce_idx));
            Halfedge_index hx = mesh.is_border(mesh.opposite(mesh.halfedge(e)))
                               ? mesh.halfedge(e) : mesh.opposite(mesh.halfedge(e));
            if (mesh.is_border(hx)) { holonomy->translations.push_back(Eigen::Vector2d::Zero()); continue; }
            Vertex_index vs = mesh.source(hx), vt = mesh.target(hx), vn = mesh.target(mesh.next(hx));
            if (!vertex_placed[vn.idx()]) { holonomy->translations.push_back(Eigen::Vector2d::Zero()); continue; }
            Eigen::Vector3d p_tri = detail::trilaterate_sph(
                result.pos[vs.idx()], result.pos[vt.idx()],
                arc_len(mesh.prev(hx)), arc_len(mesh.next(hx)));
            Eigen::Vector3d diff = p_tri - result.pos[vn.idx()];
            // Note: spherical holonomy is geometrically a 3-D rotation, not a 2-D
            // translation.  The Vector2d here stores only the (x,y) component of the
            // S²-position difference across the cut, which is an approximation.
            // For accurate spherical holonomy (rotation axis + angle) use the full
            // 3-D positions in result.pos[] directly.  Phase 10+ will replace this
            // with a proper SO(3) representation.
            holonomy->translations.push_back(Eigen::Vector2d(diff.x(), diff.y()));
        }
    }

    if (normalise) normalise_spherical(result);
    return result;
}

// ── HyperIdeal layout (Poincaré disk) — exact trilateration ──────────────────

/// Embed the mesh in the Poincaré disk (H²) using the hyperbolic metric encoded in x.
///
/// Runs priority-BFS trilateration using exact Möbius-isometric placement:
/// each new vertex is located by solving the hyperbolic law of cosines and
/// applying a Möbius map to position it in the disk.
/// For closed genus-g surfaces (g ≥ 1) a CutGraph is required.
///
/// \param mesh      Input genus-g surface mesh (g ≥ 1).
/// \param x         DOF vector returned by newton_hyper_ideal()
///                  (vertex b_v and edge a_e variables).
/// \param maps      HyperIdealMaps (lambda0, v_idx, e_idx).
/// \param cut       CutGraph from compute_cut_graph().  Required for closed surfaces.
/// \param holonomy  If non-null, receives the Möbius maps T_i ∈ SU(1,1) per cut edge.
///                  Pass to compute_period_matrix() for holonomy analysis.
/// \param normalise If true, calls normalise_hyperbolic(): iterative face-area-weighted
///                  Möbius centring (Fréchet mean, 30 iterations) → disk origin.
/// \return          Layout2D with .uv[v] ∈ Poincaré disk (|uv| < 1) for each vertex,
///                  and .halfedge_uv[h] for seam-aware texture atlasing.
///
/// \note All vertex positions satisfy |uv[v]| < 1 (inside the Poincaré disk)
///       if the metric is hyperbolic.  Points on or outside the boundary indicate
///       a non-hyperbolic metric (Gauss–Bonnet violation).
inline Layout2D hyper_ideal_layout(
    ConformalMesh&             mesh,
    const std::vector<double>& x,
    const HyperIdealMaps&      maps,
    const CutGraph*            cut      = nullptr,
    HolonomyData*              holonomy  = nullptr,
    bool                       normalise = false)
{
    const std::size_t nv = mesh.number_of_vertices();
    const std::size_t nf = mesh.number_of_faces();
    const std::size_t nh = mesh.number_of_halfedges();
    Layout2D result;
    result.uv.assign(nv, Eigen::Vector2d::Zero());
    result.halfedge_uv.assign(nh, Eigen::Vector2d::Zero());
    if (nf == 0) return result;

    auto get_b = [&](Vertex_index v) {
        int iv = maps.v_idx[v]; return (iv>=0) ? x[static_cast<std::size_t>(iv)] : 0.0; };
    auto get_a = [&](Edge_index e) {
        int ie = maps.e_idx[e]; return (ie>=0) ? x[static_cast<std::size_t>(ie)] : 0.0; };
    auto hyp_dist = [&](Halfedge_index h) {
        double bi = get_b(mesh.source(h)), bj = get_b(mesh.target(h)), a = get_a(mesh.edge(h));
        return std::acosh(std::max(1.0, std::cosh(bi+a*0.5)*std::cosh(bj+a*0.5)-std::sinh(bi)*std::sinh(bj))); };

    std::vector<bool> vertex_placed(nv, false);
    std::vector<bool> face_placed(nf, false);
    std::vector<int>  vertex_depth(nv, std::numeric_limits<int>::max());

    auto place_component = [&](Face_index f_root) {
        Halfedge_index h0 = mesh.halfedge(f_root);
        Halfedge_index h1 = mesh.next(h0), h2 = mesh.next(h1);
        Vertex_index vA = mesh.source(h0), vB = mesh.source(h1), vC = mesh.source(h2);
        double dAB = hyp_dist(h0), dBC = hyp_dist(h1), dCA = hyp_dist(h2);
        result.uv[vA.idx()] = Eigen::Vector2d::Zero();
        result.uv[vB.idx()] = Eigen::Vector2d(std::tanh(dAB*0.5), 0.0);
        result.uv[vC.idx()] = detail::trilaterate_hyp(result.uv[vA.idx()], result.uv[vB.idx()], dAB, dCA, dBC);
        vertex_placed[vA.idx()] = vertex_placed[vB.idx()] = vertex_placed[vC.idx()] = true;
        vertex_depth[vA.idx()]  = vertex_depth[vB.idx()]  = vertex_depth[vC.idx()]  = 0;
        face_placed[f_root.idx()] = true;
        detail::set_root_huv_2d(result.halfedge_uv, mesh, f_root, result.uv);

        detail::BFSQueue pq;
        auto enqueue = [&](Face_index f) {
            for (auto hf : CGAL::halfedges_around_face(mesh.halfedge(f), mesh)) {
                Halfedge_index ho = mesh.opposite(hf);
                if (mesh.is_border(ho)) continue;
                if (cut && cut->is_cut(mesh.edge(hf))) continue;
                Face_index fadj = mesh.face(ho);
                if (!face_placed[fadj.idx()]) {
                    int d = std::max(vertex_depth[mesh.source(ho).idx()],
                                     vertex_depth[mesh.target(ho).idx()]) + 1;
                    pq.push({d, ho});
                }
            }
        };
        enqueue(f_root);

        while (!pq.empty()) {
            auto [depth, h] = pq.top(); pq.pop();
            Face_index f = mesh.face(h);
            if (face_placed[f.idx()]) continue;
            Vertex_index vs = mesh.source(h), vt = mesh.target(h), vn = mesh.target(mesh.next(h));
            double D = hyp_dist(h), da = hyp_dist(mesh.prev(h)), db = hyp_dist(mesh.next(h));
            Eigen::Vector2d p = detail::trilaterate_hyp(result.uv[vs.idx()], result.uv[vt.idx()], D, da, db);
            if (!vertex_placed[vn.idx()]) {
                result.uv[vn.idx()] = p; vertex_placed[vn.idx()] = true; vertex_depth[vn.idx()] = depth;
            } else result.has_seam = true;
            detail::set_face_huv_2d(result.halfedge_uv, mesh, h, result.uv, p);
            face_placed[f.idx()] = true; enqueue(f);
        }
    };

    place_component(detail::best_root_face(mesh));
    for (auto f : mesh.faces()) if (!face_placed[f.idx()]) place_component(f);
    result.success = true;

    // ── Möbius-map holonomy ───────────────────────────────────────────────────
    if (cut && holonomy) {
        holonomy->cut_edge_indices = cut->cut_edge_indices;
        holonomy->translations.clear();
        holonomy->mobius_maps.clear();
        holonomy->mobius_maps.reserve(cut->cut_edge_indices.size());
        for (std::size_t ce_idx : cut->cut_edge_indices) {
            Edge_index e = *std::next(mesh.edges().begin(), static_cast<std::ptrdiff_t>(ce_idx));
            Halfedge_index hcut = mesh.halfedge(e), ho = mesh.opposite(hcut);
            Halfedge_index hx   = mesh.is_border(ho) ? hcut : ho;
            if (mesh.is_border(hx)) { holonomy->mobius_maps.push_back(MobiusMap::identity()); continue; }
            Vertex_index vs = mesh.source(hx), vt = mesh.target(hx), vn = mesh.target(mesh.next(hx));
            if (!vertex_placed[vn.idx()]) { holonomy->mobius_maps.push_back(MobiusMap::identity()); continue; }
            double D = hyp_dist(hx), da = hyp_dist(mesh.prev(hx)), db = hyp_dist(mesh.next(hx));
            Eigen::Vector2d p_tri = detail::trilaterate_hyp(result.uv[vs.idx()], result.uv[vt.idx()], D, da, db);
            detail::set_face_huv_2d(result.halfedge_uv, mesh, hx, result.uv, p_tri);
            using C = std::complex<double>;
            // Möbius deck transformation T across cut edge (vs,vt):
            // T is the unique Möbius isometry of the Poincaré disk that:
            //   - fixes vs and vt  (z1=w1, z2=w2: the cut-edge endpoints are
            //     identified across the seam, so T maps each to itself)
            //   - maps vn (placed side) → p_tri (virtual side)
            // This uniquely determines the hyperbolic translation/rotation
            // along the geodesic through vs and vt.
            holonomy->mobius_maps.push_back(MobiusMap::from_three(
                C(result.uv[vs.idx()].x(), result.uv[vs.idx()].y()),
                C(result.uv[vs.idx()].x(), result.uv[vs.idx()].y()),
                C(result.uv[vt.idx()].x(), result.uv[vt.idx()].y()),
                C(result.uv[vt.idx()].x(), result.uv[vt.idx()].y()),
                C(result.uv[vn.idx()].x(), result.uv[vn.idx()].y()),
                C(p_tri.x(), p_tri.y())));
        }
    }

    if (normalise) normalise_hyperbolic(result, mesh);
    return result;
}

// ── Convenience: save layout as OFF ──────────────────────────────────────────

inline void save_layout_off(
    const std::string& path, ConformalMesh& mesh, const Layout2D& layout)
{
    std::ofstream ofs(path);
    ofs << "OFF\n" << mesh.number_of_vertices() << " " << mesh.number_of_faces() << " 0\n";
    for (auto v : mesh.vertices()) { auto& p = layout.uv[v.idx()]; ofs << p.x() << " " << p.y() << " 0\n"; }
    for (auto f : mesh.faces()) {
        ofs << "3";
        for (auto h : CGAL::halfedges_around_face(mesh.halfedge(f), mesh)) ofs << " " << mesh.target(h).idx();
        ofs << "\n";
    }
}

inline void save_layout_off(
    const std::string& path, ConformalMesh& mesh, const Layout3D& layout)
{
    std::ofstream ofs(path);
    ofs << "OFF\n" << mesh.number_of_vertices() << " " << mesh.number_of_faces() << " 0\n";
    for (auto v : mesh.vertices()) { auto& p = layout.pos[v.idx()]; ofs << p.x() << " " << p.y() << " " << p.z() << "\n"; }
    for (auto f : mesh.faces()) {
        ofs << "3";
        for (auto h : CGAL::halfedges_around_face(mesh.halfedge(f), mesh)) ofs << " " << mesh.target(h).idx();
        ofs << "\n";
    }
}

} // namespace conformallab
