/*  Functions to compute k-nearest neighbours and minimum spanning trees
 *  with respect to the Euclidean metric and the thereon-based mutual
 *  reachability distances. The module gives access to a quite fast
 *  implementation of K-d trees.
 *
 *  For best speed, consider building the package from sources
 *  using, e.g., `-O3 -march=native` compiler flags.
 *
 *  Copyleft (C) 2025-2026, Marek Gagolewski <https://www.gagolewski.com>
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License
 *  Version 3, 19 November 2007, published by the Free Software Foundation.
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *  GNU Affero General Public License Version 3 for more details.
 *  You should have received a copy of the License along with this program.
 *  If this is not the case, refer to <https://www.gnu.org/licenses/>.
 */


#include "c_common.h"
#include "c_fastmst.h"
#include <cmath>

using namespace Rcpp;


//' @title
//' Get or Set the Number of Threads
//'
//' @description
//' These functions get or set the maximal number of OpenMP threads that
//' can be used by \code{\link{knn_euclid}} and \code{\link{mst_euclid}},
//' amongst others.
//'
//' @param n_threads maximal number of threads to use
//'
//' @return
//' \code{omp_get_max_threads} returns the maximal number
//' of threads that will be used during the next call to a parallelised
//' function, not the maximal number of threads possibly available.
//' It there is no built-in support for OpenMP, 1 is always returned.
//'
//' For \code{omp_set_num_threads}, the previous value of \code{max_threads}
//' is returned.
//'
//'
//' @rdname omp
//' @encoding UTF-8
//' @export
// [[Rcpp::export("omp_set_num_threads")]]
int Romp_set_num_threads(int n_threads)
{
    return Comp_set_num_threads(n_threads);
}



//' @rdname omp
//' @export
// [[Rcpp::export("omp_get_max_threads")]]
int Romp_get_max_threads()
{
    return Comp_get_max_threads();
}



//' @title Euclidean Nearest Neighbours
//'
//' @description
//' If \code{Y} is \code{NULL}, then the function determines the first \code{k}
//' nearest neighbours of each point in \code{X} with respect
//' to the Euclidean distance. It is assumed that each query point is
//' not its own neighbour.
//'
//' Otherwise, for each point in \code{Y}, this function determines the \code{k}
//' nearest points thereto from \code{X}.
//'
//' @details
//' The implemented algorithms, see the \code{algorithm} parameter, assume
//' that \eqn{k} is rather small.
//'
//' Our implementation of K-d trees (Bentley, 1975) has been quite optimised;
//' amongst others, it has good locality of reference (at the cost of making
//' a copy of the input dataset), features the sliding
//' midpoint (midrange) rule suggested by Maneewongvatana and Mound (1999),
//' node pruning strategies inspired by some ideas from (Sample et al., 2001),
//' and a couple of further tuneups proposed by the current author.
//' Still, it is well-known that K-d trees perform well only in spaces of low
//' intrinsic dimensionality.  Thus, due to the so-called curse of
//' dimensionality, for high \code{d}, the brute-force algorithm is recommended.
//'
//' The number of threads is controlled via the \code{OMP_NUM_THREADS}
//' environment variable or via the \code{\link{omp_set_num_threads}} function
//' at runtime. For best speed, consider building the package
//' from sources using, e.g., \code{-O3 -march=native} compiler flags.
//'
//'
//' @references
//' J.L. Bentley, Multidimensional binary search trees used for associative
//' searching, \emph{Communications of the ACM} 18(9), 509–517, 1975,
//' \doi{10.1145/361002.361007}
//'
//' S. Maneewongvatana, D.M. Mount, It's okay to be skinny, if your friends
//' are fat, \emph{4th CGC Workshop on Computational Geometry}, 1999
//'
//' N. Sample, M. Haines, M. Arnold, T. Purcell, Optimizing search
//' strategies in K-d Trees, \emph{5th WSES/IEEE Conf. on Circuits, Systems,
//' Communications & Computers} (CSCC'01), 2001
//'
//'
//' @param X the "database"; a matrix of shape \eqn{n\times d}
//' @param k requested number of nearest neighbours
//' @param Y the "query points"; \code{NULL} or a matrix of shape \eqn{m\times d};
//'     note that setting \code{Y=X}, contrary to \code{NULL},
//'     will include the query points themselves amongst their own neighbours
//' @param algorithm \code{"auto"}, \code{"kd_tree"} or \code{"brute"};
//'     K-d trees can be used for \code{d} between 2 and 20 only;
//'     \code{"auto"} selects \code{"kd_tree"} in low-dimensional spaces
//' @param max_leaf_size maximal number of points in the K-d tree leaves;
//'     smaller leaves use more memory, yet are not necessarily faster;
//'     use \code{0} to select the default value, currently set to 32
//' @param squared whether the output \code{nn.dist} should be based on
//'     the squared Euclidean distance
//' @param verbose whether to print diagnostic messages
//'
//'
//' @return
//' A list with two elements, \code{nn.index} and \code{nn.dist}, is returned.
//'
//' \code{nn.dist} and \code{nn.index} have shape \eqn{n\times k}
//' or \eqn{m\times k}, depending whether \code{Y} is given.
//'
//' \code{nn.index[i,j]} is the index (between \eqn{1} and \eqn{n})
//' of the \eqn{j}-th nearest neighbour of \eqn{i}.
//'
//' \code{nn.dist[i,j]} gives the weight of the edge \code{{i, nn.index[i,j]}},
//' i.e., the distance between the \eqn{i}-th point and its \eqn{j}-th
//' nearest neighbour, \eqn{j=1,\dots,k}.
//' \code{nn.dist[i,]} is sorted nondecreasingly for all \eqn{i}.
//'
//'
//'
//' @examples
//' library("datasets")
//' data("iris")
//' X <- jitter(as.matrix(iris[1:2]))  # some data
//' neighbours <- knn_euclid(X, 1)  # 1-NNs of each point
//' plot(X, asp=1, las=1)
//' segments(X[,1], X[,2], X[neighbours$nn.index,1], X[neighbours$nn.index,2])
//'
//' knn_euclid(X, 5, matrix(c(6, 4), nrow=1))  # five closest points to (6, 4)
//'
//'
//' @seealso \code{\link{mst_euclid}}
//'
//' @rdname knn_euclid
//' @encoding UTF-8
//' @export
// [[Rcpp::export("knn_euclid")]]
List knn_euclid(
    SEXP X,
    int k=1,
    SEXP Y=R_NilValue,
    Rcpp::String algorithm="auto",
    int max_leaf_size=0,
    bool squared=false,
    bool verbose=false
) {
    using FLOAT = double;  // float is not faster..

    Rcpp::NumericMatrix _X;
    if (!Rf_isMatrix(X))  _X = Rcpp::internal::convert_using_rfunction(X, "as.matrix");
    else _X = X;

    Py_ssize_t n = (Py_ssize_t)_X.nrow();
    Py_ssize_t d = (Py_ssize_t)_X.ncol();
    Py_ssize_t m;
    bool use_kdtree;

    if (n < 1 || d <= 1) stop("X is ill-shaped");
    if (k < 1) stop("`k` must be >= 1");

    if (algorithm == "auto") {
        if (2 <= d && d <= 20)
            algorithm = "kd_tree";
        else
            algorithm = "brute";
    }

    if (algorithm == "kd_tree") {
        if (d < 2 || d > 20) stop("kd_tree can only be used for 2 <= d <= 20");
        if (max_leaf_size == 0) max_leaf_size = 32;  // the current default

        if (max_leaf_size <= 0) stop("max_leaf_size must be positive");
        use_kdtree = true;
    }
    else if (algorithm == "brute")
        use_kdtree = false;
    else
        stop("invalid 'algorithm'");


    std::vector<FLOAT> XC(n*d);
    Py_ssize_t j = 0;
    for (Py_ssize_t i=0; i<n; ++i)
        for (Py_ssize_t u=0; u<d; ++u)
            XC[j++] = (FLOAT)_X(i, u);  // row-major


    std::vector<FLOAT>      nn_dist;
    std::vector<Py_ssize_t> nn_ind;
    if (Rf_isNull(Y)) {
        if (k >= n) stop("too many neighbours requested");
        m = n;

        nn_dist.resize(n*k);
        nn_ind.resize(n*k);

        if (use_kdtree)
            Cknn1_euclid_kdtree(
                XC.data(), n, d, k, nn_dist.data(),
                nn_ind.data(), max_leaf_size, squared, verbose
            );
        else
            Cknn1_euclid_brute(
                XC.data(), n, d, k, nn_dist.data(),
                nn_ind.data(), squared, verbose
            );
    }
    else {
        if (k >  n) stop("too many neighbours requested");

        Rcpp::NumericMatrix _Y;
        if (!Rf_isMatrix(Y)) _Y = Rcpp::internal::convert_using_rfunction(Y, "as.matrix");
        else _Y = Y;

        m = (Py_ssize_t)_Y.nrow();
        if (_Y.ncol() != d) stop("Y's dimensionality does not match that of X");

        nn_dist.resize(m*k);
        nn_ind.resize(m*k);

        std::vector<FLOAT> YC(m*d);
        Py_ssize_t j = 0;
        for (Py_ssize_t i=0; i<m; ++i)
            for (Py_ssize_t u=0; u<d; ++u)
                YC[j++] = (FLOAT)_Y(i, u);  // row-major

        if (use_kdtree)
            Cknn2_euclid_kdtree(
                XC.data(), n, YC.data(), m, d, k, nn_dist.data(), nn_ind.data(),
                max_leaf_size, squared, verbose
            );
        else
            Cknn2_euclid_brute(
                XC.data(), n, YC.data(), m, d, k, nn_dist.data(), nn_ind.data(),
                squared, verbose
            );
    }

    Rcpp::IntegerMatrix out_ind(m, k);
    Rcpp::NumericMatrix out_dist(m, k);
    Py_ssize_t u = 0;
    for (Py_ssize_t i=0; i<m; ++i) {
        for (Py_ssize_t j=0; j<k; ++j) {
            out_ind(i, j)  = nn_ind[u]+1.0;  // R-based indexing
            out_dist(i, j) = nn_dist[u];
            u++;
        }
    }

    return Rcpp::List::create(
        Rcpp::Named("nn.index")=out_ind,
        Rcpp::Named("nn.dist")=out_dist
    );
}




//' @title Euclidean and Mutual Reachability Minimum Spanning Trees
//'
//' @description
//' The function determines the/a(*) minimum spanning tree (MST) of a set
//' of \eqn{n} points, i.e., an acyclic undirected connected graph whose
//' vertices represent the points, and edges are weighted by the distances
//' between point pairs and have minimal total weight.
//'
//' MSTs have many uses in, amongst others, topological data analysis
//' (clustering, density estimation, dimensionality reduction,
//' outlier detection, etc.).
//'
//' In clustering and density estimation, the parameter \code{M} plays the role
//' of a smoothing factor; for discussion, see (Campello et al., 2015)
//' and the references therein.
//'
//' For \eqn{M\leq 1}, we get a spanning tree that minimises the sum of
//' Euclidean distances between the points, i.e., the classic Euclidean minimum
//' spanning tree (EMST).   If \eqn{M=1}, the function additionally returns
//' the distance to each point's nearest neighbour.
//'
//' If \eqn{M>1}, the spanning tree is the smallest with respect to
//' the degree-\eqn{M} mutual reachability distance (Campello et al., 2013) given by
//' \eqn{d_M(i, j)=\max\{ c_M(i), c_M(j), d(i, j)\}}, where \eqn{d(i,j)}
//' is the standard Euclidean distance between the \eqn{i}-th and the \eqn{j}-th point,
//' and \eqn{c_M(i)} is the \eqn{i}-th \eqn{M}-core distance defined as the distance
//' between the \eqn{i}-th point and its \eqn{M}-th nearest neighbour
//' (not including the query point itself).
//'
//' Note that (Campello et al., 2013) defines the core distance as the
//' distance to the \eqn{(M-1)}-th nearest neighbour (or the \eqn{M}-th one,
//' but including self).
//'
//'
//' @details
//' (*) Note that if there are many pairs of equidistant points,
//' there can be many minimum spanning trees. In particular, it is likely
//' that there are point pairs with the same mutual reachability distances.
//'
//' To make the definition unambiguous, the \code{mutreach_ties} argument
//' indicates the preference towards connecting to farther/closer points with
//' respect to the original metric, or having smaller/larger core distances
//' in cases of tied distances; see (Gagolewski, 2026).  Empirically,
//' \code{mutreach_ties="dcore_min"} and \code{mutreach_leaves="reconnect_dcore_min"}
//' leads to MSTs with more leaves and hubs.
//'
//' The brute force method always resolves all ties, whilst, for efficiency,
//' the K-d tree-based algorithms use this adjustment only for the first \eqn{M}
//' nearest neighbours, so the resulting trees might be slightly different.
//'
//' The implemented algorithms, see the \code{algorithm} parameter, assume
//' that \eqn{M} is rather small.
//'
//' Our implementation of K-d trees (Bentley, 1975) has been quite optimised;
//' amongst others, it has good locality of reference (at the cost of making
//' a copy of the input dataset), features the sliding
//' midpoint (midrange) rule suggested by Maneewongvatana and Mound (1999),
//' node pruning strategies inspired by some ideas from (Sample et al., 2001),
//' and a couple of further tuneups proposed by the current author.
//'
//' The "single-tree" version of the Borůvka algorithm is parallelised:
//' in every iteration, it seeks each point's nearest "alien",
//' i.e., the nearest point thereto from another cluster.
//' The "dual-tree" Borůvka version of the algorithm is, in principle, based
//' on (March et al., 2010). As far as our implementation is concerned,
//' the dual-tree approach is often only faster in 2- and 3-dimensional spaces,
//' for \eqn{M\leq 1}, and in a single-threaded setting.  For another
//' (approximate) adaptation of the dual-tree algorithm to mutual
//' reachability distances, see (McInnes and Healy, 2017).
//'
//' The "sesqui-tree" variant (by the current author) is a mixture of the two
//' approaches:  it compares leaves against the full tree and can be run
//' in parallel.  It is usually faster than the single- and dual-tree methods
//' in very low dimensional spaces and usually not much slower than
//' the single-tree variant otherwise.
//'
//' Nevertheless, it is well-known that K-d trees perform well only in spaces
//' of low intrinsic dimensionality (the "curse").  For high \eqn{d},
//' the "brute-force" algorithm is recommended.  Here, we provided a
//' parallelised (see Olson, 1995) version of the Jarník (1930) (a.k.a.
//' Prim, 1957) algorithm, where the distances are computed
//' on the fly (only once for \eqn{M\leq 1}).
//'
//' The number of threads used is controlled via the \code{OMP_NUM_THREADS}
//' environment variable or via the \code{\link{omp_set_num_threads}} function
//' at runtime. For best speed, consider building the package
//' from sources using, e.g., \code{-O3 -march=native} compiler flags.
//'
//'
//' @references
//' V. Jarník, O jistém problému minimálním,
//' \emph{Práce Moravské Přírodovědecké Společnosti} 6, 1930, 57–63.
//'
//' C.F. Olson, Parallel algorithms for hierarchical clustering,
//' Parallel Computing 21(8), 1995, 1313–1325.
//'
//' R. Prim, Shortest connection networks and some generalizations,
//' \emph{The Bell System Technical Journal} 36(6), 1957, 1389–1401.
//'
//' O. Borůvka, O jistém problému minimálním, \emph{Práce Moravské
//' Přírodovědecké Společnosti} 3, 1926, 37–58.
//'
//' W.B. March, R. Parikshit, A.G. Gray, Fast Euclidean minimum spanning
//' tree: Algorithm, analysis, and applications, \emph{Proc. 16th ACM SIGKDD
//' Intl. Conf. Knowledge Discovery and Data Mining (KDD '10)}, 2010, 603–612.
//'
//' J.L. Bentley, Multidimensional binary search trees used for associative
//' searching, \emph{Communications of the ACM} 18(9), 509–517, 1975,
//' \doi{10.1145/361002.361007}
//'
//' S. Maneewongvatana, D.M. Mount, It's okay to be skinny, if your friends
//' are fat, \emph{4th CGC Workshop on Computational Geometry}, 1999
//'
//' N. Sample, M. Haines, M. Arnold, T. Purcell, Optimizing search
//' strategies in K-d Trees, \emph{5th WSES/IEEE Conf. on Circuits, Systems,
//' Communications & Computers} (CSCC'01), 2001
//'
//' R.J.G.B. Campello, D. Moulavi, J. Sander, Density-based clustering based
//' on hierarchical density estimates, \emph{Lecture Notes in Computer Science}
//' 7819, 2013, 160–172. \doi{10.1007/978-3-642-37456-2_14}
//'
//' R.J.G.B. Campello, D. Moulavi, A. Zimek, J. Sander, Hierarchical
//' density estimates for data clustering, visualization, and outlier detection,
//' \emph{ACM Transactions on Knowledge Discovery from Data (TKDD)} 10(1),
//' 2015, 1–51, \doi{10.1145/2733381}
//'
//' L. McInnes, J. Healy, Accelerated hierarchical density-based
//' clustering, \emph{IEEE Intl. Conf. Data Mining Workshops (ICMDW)}, 2017,
//' 33–42, \doi{10.1109/ICDMW.2017.12}
//'
//' M. Gagolewski, quitefastmst, in preparation, 2026, TODO
//'
//'
//' @param X the "database"; a matrix of shape \eqn{n\times d}
//' @param M the smoothing factor a.k.a. the degree of the mutual reachability
//'    distance; \eqn{M\leq 1} gives the ordinary Euclidean distance
//' @param algorithm \code{"auto"}, \code{"single_kd_tree"},
//'    \code{"sesqui_kd_tree"}, \code{"dual_kd_tree"}, or \code{"brute"};
//'    K-d trees can only be used for \eqn{d} between 2 and 20 only;
//'    \code{"auto"} selects \code{"sesqui_kd_tree"} for \eqn{d\leq 20}.
//'    \code{"brute"} is used otherwise
//' @param max_leaf_size maximal number of points in the K-d tree leaves;
//'    smaller leaves use more memory, yet are not necessarily faster;
//'    use \code{0} to select the default value, currently set to 32 for the
//'    single-tree and sesqui-tree and 8 for the dual-tree Borůvka algorithm
//' @param first_pass_max_brute_size minimal number of points in a node to
//'    treat it as a leaf (unless it actually is a leaf) in the first
//'    iteration of the algorithm; use \code{0} to select the default value,
//'    currently set to 32
//' @param mutreach_ties adjustment for mutual reachability distance ambiguity
//'    (for \eqn{M>1}); one of \code{"dcore_min"}, \code{"dist_max"},
//'    \code{"dist_min"} (default), or \code{"dcore_max"}
//' @param mutreach_leaves a way to postprocess the leaves of the computed tree;
//'    one of \code{"keep"} (default: do nothing),
//'    or \code{"reconnect_dcore_min"} (try reconnecting leaves to
//'    inner vertices which have them amongst their M nearest neighbours;
//'    prefer vertices of the smallest core distance)
//' @param verbose whether to print diagnostic messages
//'
//'
//' @return
//' A list with two $(M=0)$ or four $(M>0)$ elements, \code{mst.index} and
//' \code{mst.dist}, and additionally \code{nn.index} and \code{nn.dist}.
//'
//' \code{mst.index} is a matrix with \eqn{n-1} rows and \eqn{2} columns,
//' whose rows define the tree edges.
//'
//' \code{mst.dist} is a vector of length
//' \eqn{n-1} giving the weights of the corresponding edges.
//'
//' The tree edges are ordered with respect to weights nondecreasingly, and then by
//' the indexes (lexicographic ordering of the \code{(weight, index1, index2)}
//' triples).  For each \code{i}, it holds \code{mst_ind[i,1]<mst_ind[i,2]}.
//'
//' \code{nn.index} is an \eqn{n} by \eqn{M} matrix giving the indexes
//' of each point's nearest neighbours with respect to the Euclidean distance.
//' \code{nn.dist} provides the corresponding Euclidean distances.
//'
//'
//' @examples
//' library("datasets")
//' data("iris")
//' X <- jitter(as.matrix(iris[1:2]))  # some data
//' T <- mst_euclid(X)                 # Euclidean MST of X
//' plot(X, asp=1, las=1)
//' segments(X[T$mst.index[, 1], 1], X[T$mst.index[, 1], 2],
//'          X[T$mst.index[, 2], 1], X[T$mst.index[, 2], 2])
//'
//' @seealso \code{\link{knn_euclid}}
//'
//' @rdname mst_euclid
//' @encoding UTF-8
//' @export
// [[Rcpp::export("mst_euclid")]]
List mst_euclid(
    SEXP X,
    int M=0,
    Rcpp::String algorithm="auto",
    int max_leaf_size=0,
    int first_pass_max_brute_size=0,
    Rcpp::String mutreach_ties="dist_min",
    Rcpp::String mutreach_leaves="keep",
    bool verbose=false
) {
    using FLOAT = double;  // float is not faster..

    Rcpp::NumericMatrix _X;
    if (!Rf_isMatrix(X)) _X = Rcpp::internal::convert_using_rfunction(X, "as.matrix");
    else _X = X;

    Py_ssize_t n = (Py_ssize_t)_X.nrow();
    Py_ssize_t d = (Py_ssize_t)_X.ncol();
    bool use_kdtree = true;
    FLOAT boruvka_variant = 1.5;
    Py_ssize_t mutreach_ties_val = -2;

    if (n < 1 || d <= 1)   stop("X is ill-shaped");
    if (M < 0 || M >= n)   stop("incorrect M");

    if (algorithm == "auto") {
        if (2 <= d && d <= 20) {
            //if (d <= 3)
            algorithm = "sesqui_kd_tree";
            //else
            //    algorithm = "single_kd_tree";
        }
        else
            algorithm = "brute";
    }

    if (algorithm == "single_kd_tree" || algorithm == "sesqui_kd_tree" || algorithm == "dual_kd_tree") {
        if (d < 2 || d > 20) stop("K-d trees can only be used for 2 <= d <= 20");
        use_kdtree = true;

        if (algorithm == "single_kd_tree") {
            if (max_leaf_size == 0)
                max_leaf_size = 32;  // the current default
            if (first_pass_max_brute_size == 0)
                first_pass_max_brute_size = 32;  // the current default
            boruvka_variant = 1.0;
        }
        else if (algorithm == "sesqui_kd_tree") {
            if (max_leaf_size == 0)
                max_leaf_size = 32;  // the current default
            if (first_pass_max_brute_size == 0)
                first_pass_max_brute_size = 32;  // the current default
            boruvka_variant = 1.5;
        }
        else {
            if (max_leaf_size == 0)
                max_leaf_size = 8;  // the current default
            if (first_pass_max_brute_size == 0)
                first_pass_max_brute_size = 32;  // the current default
            boruvka_variant = 2.0;
        }

        if (max_leaf_size <= 0)
            stop("max_leaf_size must be positive");
        if (first_pass_max_brute_size <= 0)
            stop("first_pass_max_brute_size must be positive");
    }
    else if (algorithm == "brute")
        use_kdtree = false;
    else
        stop("invalid 'algorithm'");


    if (mutreach_ties == "dcore_min")
        mutreach_ties_val = -2;
    else if (mutreach_ties == "dist_max")
        mutreach_ties_val = -1;
    else if (mutreach_ties == "dist_min")
        mutreach_ties_val = 1;
    else if (mutreach_ties == "dcore_max")
        mutreach_ties_val = 2;
    else
        stop("invalid 'mutreach_ties'");

    std::vector<FLOAT> XC(n*d);
    Py_ssize_t j = 0;
    for (Py_ssize_t i=0; i<n; ++i)
        for (Py_ssize_t u=0; u<d; ++u)
            XC[j++] = (FLOAT)_X(i, u);  // row-major


    std::vector<Py_ssize_t> mst_ind((n-1)*2);    // C-order
    std::vector<FLOAT>      mst_dist(n-1);       // TODO: use out_dist
    std::vector<Py_ssize_t> nn_ind((M==0)?0:(n*M));
    std::vector<FLOAT>      nn_dist((M==0)?0:(n*M));

    if (use_kdtree)
        Cmst_euclid_kdtree(
            XC.data(), n, d, M, mst_dist.data(), mst_ind.data(),
            (M==0)?nullptr:nn_dist.data(), (M==0)?nullptr:nn_ind.data(),
            max_leaf_size, first_pass_max_brute_size, boruvka_variant,
            mutreach_ties_val, verbose
        );
    else
        Cmst_euclid_brute(
            XC.data(), n, d, M, mst_dist.data(), mst_ind.data(),
            (M==0)?nullptr:nn_dist.data(), (M==0)?nullptr:nn_ind.data(),
            mutreach_ties_val, verbose
        );

    if (mutreach_leaves=="keep")
        ;
    else if (mutreach_leaves=="reconnect_dcore_min") {
        if (M>0) {
            Py_ssize_t mutreach_leaves_maxiter = 10;   // TODO: param
            while (mutreach_leaves_maxiter > 0) {
                Py_ssize_t num_changes = Cleaves_reconnect_dcore_min(
                    n-1, n, M,
                    mst_dist.data(), mst_ind.data(),
                    nn_dist.data(), nn_ind.data()
                );
                if (num_changes <= 0) break;
                mutreach_leaves_maxiter--;
            }
        }
    }
    else
        stop("invalid 'mutreach_leaves'");

    // generate outputs
    Rcpp::IntegerMatrix out_mst_ind(n-1, 2);
    Rcpp::NumericVector out_mst_dist(n-1);
    for (Py_ssize_t i=0; i<n-1; ++i) {
        out_mst_ind(i, 0)  = mst_ind[2*i+0]+1.0;  // R-based indexing
        out_mst_ind(i, 1)  = mst_ind[2*i+1]+1.0;  // R-based indexing
        out_mst_dist(i)    = mst_dist[i];
    }

    if (M == 0) {
        return Rcpp::List::create(
            Rcpp::Named("mst.index")=out_mst_ind,
            Rcpp::Named("mst.dist") =out_mst_dist
        );
    }
    else {
        Rcpp::IntegerMatrix out_nn_ind(n, M);
        Rcpp::NumericMatrix out_nn_dist(n, M);
        Py_ssize_t u=0;
        for (Py_ssize_t i=0; i<n; ++i) {
            for (Py_ssize_t j=0; j<M; ++j) {
                out_nn_ind(i, j)  = nn_ind[u]+1;  // 1-based indexing
                out_nn_dist(i, j) = nn_dist[u];
                ++u;
            }
        }
        return Rcpp::List::create(
            Rcpp::Named("mst.index")=out_mst_ind,
            Rcpp::Named("mst.dist") =out_mst_dist,
            Rcpp::Named("nn.index") =out_nn_ind,
            Rcpp::Named("nn.dist")  =out_nn_dist
        );
    }
}
