#pragma once
// Copyright (c) 2024-2026 Tarik Moussa.
// SPDX-License-Identifier: MIT

// inversive_distance_hessian.hpp
//
// Phase 9a.2 — Hessian of the inversive-distance circle-packing functional
//              (Luo 2004 / Bowers-Stephenson 2004).
//
// ┌──────────────────────────────────────────────────────────────────────────┐
// │  Implementation strategy                                                  │
// │                                                                           │
// │  TWO finite-difference Hessian implementations are provided here,         │
// │  mirroring the HyperIdeal pair in `hyper_ideal_hessian.hpp`:              │
// │                                                                           │
// │    1.  `inversive_distance_hessian` — full finite-difference baseline.    │
// │        Cost ≈ n × (cost of a full gradient evaluation) = O(n · F).        │
// │        Kept as the cross-validation reference for the block-FD variant.   │
// │                                                                           │
// │    2.  `inversive_distance_hessian_block_fd` — per-face block-FD.         │
// │        Each face contributes to the gradient through exactly 3 vertex     │
// │        DOFs (u₁,u₂,u₃); we FD the 3×3 local Jacobian of that face's       │
// │        gradient contribution and scatter it.  Cost ≈ F × 6 face-angle     │
// │        evaluations = O(F).  Speed-up factor ≈ n/6 over full-FD.           │
// │                                                                           │
// │  Why the block-FD is correct (locality lemma):                            │
// │     G_v = Θ_v − Σ_{f ∋ v} α_v(f),   and α_v(f) depends ONLY on the 3      │
// │  vertex DOFs of face f.  Hence  ∂G_x/∂y = Σ_{f: x,y ∈ {v1,v2,v3}(f)}      │
// │  ∂(−α_x)/∂y at f, so accumulating per-face 3×3 blocks reproduces the      │
// │  full Hessian (identical to O(ε²)).                                       │
// │                                                                           │
// │  An analytic Hessian via Glickenstein 2011 eq. (4.6) is tracked in        │
// │  `doc/roadmap/research-track.md` as Phase 9a.2-analytic; it would take     │
// │  the cost from O(F)·(FD constant) to a single O(F) analytic pass.         │
// └──────────────────────────────────────────────────────────────────────────┘

#include "inversive_distance_functional.hpp"
#include <Eigen/Sparse>
#include <vector>
#include <cmath>

namespace conformallab {

/// Full finite-difference Inversive-Distance Hessian (baseline).
/// Cost: `n` full-gradient evaluations ≈ `O(n·F)`.  Use for small meshes
/// or as a correctness reference for the block-FD variant.
inline Eigen::SparseMatrix<double> inversive_distance_hessian(
    const ConformalMesh&         mesh,
    const std::vector<double>&   x,
    const InversiveDistanceMaps& m,
    double                       eps = 1e-5)
{
    const int n = inversive_distance_dimension(mesh, m);
    std::vector<Eigen::Triplet<double>> trips;
    trips.reserve(static_cast<std::size_t>(n) * 16);

    std::vector<double> xp = x, xm = x;
    for (int j = 0; j < n; ++j) {
        const std::size_t sj = static_cast<std::size_t>(j);
        xp[sj] = x[sj] + eps;
        xm[sj] = x[sj] - eps;

        auto Gp = inversive_distance_gradient(mesh, xp, m);
        auto Gm = inversive_distance_gradient(mesh, xm, m);

        xp[sj] = xm[sj] = x[sj];   // restore

        for (int i = 0; i < n; ++i) {
            double val = (Gp[static_cast<std::size_t>(i)]
                        - Gm[static_cast<std::size_t>(i)]) / (2.0 * eps);
            if (std::abs(val) > 1e-15)
                trips.emplace_back(i, j, val);
        }
    }

    Eigen::SparseMatrix<double> H(n, n);
    H.setFromTriplets(trips.begin(), trips.end());
    return H;
}

/// Symmetrised full-FD Inversive-Distance Hessian: `(H + Hᵀ)/2`.
inline Eigen::SparseMatrix<double> inversive_distance_hessian_sym(
    const ConformalMesh&         mesh,
    const std::vector<double>&   x,
    const InversiveDistanceMaps& m,
    double                       eps = 1e-5)
{
    auto H = inversive_distance_hessian(mesh, x, m, eps);
    Eigen::SparseMatrix<double> Ht = H.transpose();
    return (H + Ht) * 0.5;
}

// ── Block-FD Hessian ──────────────────────────────────────────────────────────
//
// The 3 local DOFs of a face f are (u_{v1}, u_{v2}, u_{v3}).  For each free
// local DOF we recompute the face's gradient contribution (−α₁,−α₂,−α₃) at
// x ± ε along that axis and read off the 3×3 Jacobian.  The result scatters
// into the global Hessian via the DOF-index lookup.
//
// Cost: F × 6 face-angle evaluations (3 DOFs × 2 directions) vs n×F for
// full-FD — a speed-up of ≈ n/6, i.e. ~hundreds× on large closed meshes.
/// Per-face block-FD Inversive-Distance Hessian.  Uses the locality lemma
/// `∂G_x/∂y = Σ_{f: x,y ∈ {v1,v2,v3}(f)} ∂(−α_x)/∂y` to perturb only the 3
/// face-local DOFs at a time, giving an `F·6` face-evaluation budget vs `n·F`
/// for full-FD.  Mathematically equivalent to `inversive_distance_hessian`
/// up to O(ε²) FD rounding.
inline Eigen::SparseMatrix<double> inversive_distance_hessian_block_fd(
    const ConformalMesh&         mesh,
    const std::vector<double>&   x,
    const InversiveDistanceMaps& m,
    double                       eps = 1e-5)
{
    const int n = inversive_distance_dimension(mesh, m);
    std::vector<Eigen::Triplet<double>> trips;
    trips.reserve(9 * mesh.number_of_faces());

    for (auto f : mesh.faces()) {
        Halfedge_index h0 = mesh.halfedge(f);
        Halfedge_index h1 = mesh.next(h0);
        Halfedge_index h2 = mesh.next(h1);

        Vertex_index v1 = mesh.source(h0);
        Vertex_index v2 = mesh.source(h1);
        Vertex_index v3 = mesh.source(h2);
        Edge_index   e12 = mesh.edge(h0);
        Edge_index   e23 = mesh.edge(h1);
        Edge_index   e31 = mesh.edge(h2);

        // Local DOF indices: (u1, u2, u3).  Pinned slots = -1.
        const int idx[3] = { m.v_idx[v1], m.v_idx[v2], m.v_idx[v3] };

        const double I12 = m.I_e[e12];
        const double I23 = m.I_e[e23];
        const double I31 = m.I_e[e31];

        // Local DOF values (0 for pinned).
        const double vals[3] = {
            id_detail::dof_val(idx[0], x),
            id_detail::dof_val(idx[1], x),
            id_detail::dof_val(idx[2], x)
        };

        for (int j = 0; j < 3; ++j) {
            if (idx[j] < 0) continue;   // never perturb a pinned DOF

            double vp[3], vm[3];
            for (int k = 0; k < 3; ++k) { vp[k] = vm[k] = vals[k]; }
            vp[j] += eps;
            vm[j] -= eps;

            auto Cp = inversive_distance_face_grad_contribs(
                vp[0], vp[1], vp[2], I12, I23, I31);
            auto Cm = inversive_distance_face_grad_contribs(
                vm[0], vm[1], vm[2], I12, I23, I31);

            const double Gp[3] = { Cp.g1, Cp.g2, Cp.g3 };
            const double Gm[3] = { Cm.g1, Cm.g2, Cm.g3 };

            for (int i = 0; i < 3; ++i) {
                if (idx[i] < 0) continue;   // pinned: contributes nothing
                const double val = (Gp[i] - Gm[i]) / (2.0 * eps);
                if (std::abs(val) > 1e-15)
                    trips.emplace_back(idx[i], idx[j], val);
            }
        }
    }

    Eigen::SparseMatrix<double> H(n, n);
    H.setFromTriplets(trips.begin(), trips.end());
    return H;
}

/// Symmetrised block-FD Inversive-Distance Hessian: `(H + Hᵀ)/2` of
/// `inversive_distance_hessian_block_fd(...)` for solvers requiring strict
/// symmetry.
inline Eigen::SparseMatrix<double> inversive_distance_hessian_block_fd_sym(
    const ConformalMesh&         mesh,
    const std::vector<double>&   x,
    const InversiveDistanceMaps& m,
    double                       eps = 1e-5)
{
    auto H = inversive_distance_hessian_block_fd(mesh, x, m, eps);
    Eigen::SparseMatrix<double> Ht = H.transpose();
    return (H + Ht) * 0.5;
}

} // namespace conformallab
