#include <RcppArmadillo.h>
#include <algorithm>
#include <cmath>
#include <limits>
#include <vector>
#include "gpf_engine.h"
// [[Rcpp::depends(RcppArmadillo)]]
// [[Rcpp::plugins(cpp14)]]

using namespace Rcpp;
using namespace arma;

// Gradient-projection factor rotation along the orthogonal (Stiefel) manifold,
// generalized over an arbitrary rotation criterion (Bernaards & Jennrich, 2005). A
// criterion exposes the objective value f and its gradient Gq = dQ/dL at the rotated
// loadings L; the engine maps Gq to the gradient with respect to the orthogonal
// transformation T, projects it onto the tangent space, performs a non-monotone
// line search, and retracts back onto the orthogonal group via a polar
// (SVD) projection. This mirrors GPArotation::GPForth(); only the criterion functor
// changes between rotation methods.

// A rotation criterion: value f and gradient Gq = dQ/dL at the rotated loadings L.
struct RotationCriterion {
  virtual ~RotationCriterion() {}
  virtual void eval(const arma::mat& L, double& f, arma::mat& Gq) const = 0;
};

// Crawford-Ferguson family, parameterized by kappa (GPArotation::vgQ.cf). With
// N = 1_k 1_k' - I_k and M = 1_p 1_p' - I_p the criterion balances row complexity
// (between-factor cross-products of squared loadings) against column complexity
// (between-variable cross-products). kappa = 0 is quartimax; kappa = k / (2 p) is
// equamax.
struct CfCriterion : public RotationCriterion {
  double kappa_;
  explicit CfCriterion(double kappa) : kappa_(kappa) {}

  void eval(const arma::mat& L, double& f, arma::mat& Gq) const override {
    // With N = 1_k 1_k' - I_k and M = 1_p 1_p' - I_p, the products L2 %*% N and
    // M %*% L2 are sum-broadcasts of L2: (L2 N)(i,j) = rowSum_i(L2) - L2(i,j) and
    // (M L2)(i,j) = colSum_j(L2) - L2(i,j). Forming them from the row/column sums
    // avoids building the p x p / k x k constant matrices and the dense products on
    // every evaluation (this is the innermost kernel of the multi-start solver).
    arma::mat L2 = square(L);
    arma::vec row_sums = sum(L2, 1);
    arma::rowvec col_sums = sum(L2, 0);
    arma::mat L2N = -L2;
    L2N.each_col() += row_sums;
    arma::mat ML2 = -L2;
    ML2.each_row() += col_sums;
    double f1 = (1.0 - kappa_) * accu(L2 % L2N) / 4.0;
    double f2 = kappa_ * accu(L2 % ML2) / 4.0;
    f = f1 + f2;
    Gq = (1.0 - kappa_) * (L % L2N) + kappa_ * (L % ML2);
  }
};

// Oblimin family, parameterized by gamma (GPArotation::vgQ.oblimin). With
// N = 1_k 1_k' - I_k the criterion penalizes the between-factor cross-products of squared
// loadings; gamma centers those cross-products across variables. gamma = 0 is the
// quartimin criterion. As in CfCriterion the dense N / constant matrices are avoided: the
// product X = L2 %*% N is the sum-broadcast X(i,j) = rowSum_i(L2) - L2(i,j), and when
// gamma != 0 the premultiplication by (I_p - (gamma / p) 1_p 1_p') subtracts the
// (gamma / p)-scaled column sums of X.
struct ObliminCriterion : public RotationCriterion {
  double gamma_;
  explicit ObliminCriterion(double gamma) : gamma_(gamma) {}

  void eval(const arma::mat& L, double& f, arma::mat& Gq) const override {
    const double p = static_cast<double>(L.n_rows);
    arma::mat L2 = square(L);
    arma::vec row_sums = sum(L2, 1);
    arma::mat X = -L2;
    X.each_col() += row_sums;
    if (gamma_ != 0.0) {
      arma::rowvec col_sums = sum(X, 0);
      X.each_row() -= (gamma_ / p) * col_sums;
    }
    f = accu(L2 % X) / 4.0;
    Gq = L % X;
  }
};

// Geomin family, parameterized by delta (GPArotation::vgQ.geomin). The criterion is the sum
// over variables of the geometric mean of their squared loadings, each offset by delta:
// f = sum_i exp(mean_j log(L2_ij)) with L2 = L^2 + delta. The offset keeps the row geometric
// means (and their logarithms) finite when a loading is zero; smaller delta rewards a sparser
// pattern but sharpens the local minima. delta = 0.01 is the GPArotation default. With the
// per-row geometric mean pro_i = exp(mean_j log(L2_ij)), the gradient is (2 / k)(L / L2) with
// each row scaled by pro_i.
struct GeominCriterion : public RotationCriterion {
  double delta_;
  explicit GeominCriterion(double delta) : delta_(delta) {}

  void eval(const arma::mat& L, double& f, arma::mat& Gq) const override {
    const double k = static_cast<double>(L.n_cols);
    // delta > 0 (validated by the entry points) keeps L2 strictly positive, so log(L2) and
    // the geometric means below are always finite.
    arma::mat L2 = square(L);
    L2 += delta_;
    arma::vec pro = exp(mean(log(L2), 1));  // dim = 1: per-row mean of log(L2) (rowMeans)
    f = accu(pro);
    Gq = (2.0 / k) * (L / L2);
    Gq.each_col() %= pro;  // scale row i by pro_i (pro broadcast across columns)
  }
};

// Bentler's invariant pattern simplicity criterion (GPArotation::vgQ.bentler). With the k x k
// cross-product of squared loadings M = L2' L2 and its diagonal D, the criterion is
// f = -(log|M| - log|D|) / 4: it is minimized when M is as close to diagonal as possible, i.e.
// the columns of squared loadings are mutually orthogonal (an invariant-pattern notion of simple
// structure). The gradient is Gq = -L .* (L2 (M^-1 - D^-1)). A degenerate M -- a rank-deficient
// cross-product or a zero column of squared loadings (a zero diagonal entry) -- leaves the
// criterion undefined; this is signalled by an infinite objective and a zero gradient so the
// manifold's validity check rejects the candidate, mirroring the invalid-state handling of the
// other criteria rather than throwing.
struct BentlerCriterion : public RotationCriterion {
  void eval(const arma::mat& L, double& f, arma::mat& Gq) const override {
    arma::mat L2 = square(L);
    arma::mat M = L2.t() * L2;  // k x k cross-product of squared loadings
    arma::vec dM = M.diag();

    double logDetM, sgn;
    bool det_ok = arma::log_det(logDetM, sgn, M);  // logDetM = log|det(M)|, sgn = sign(det(M))
    arma::mat Minv;
    bool inv_ok = inverse_checked_cpp(M, Minv);

    if (!det_ok || sgn <= 0.0 || !std::isfinite(logDetM) || !inv_ok || dM.min() <= 0.0) {
      f = std::numeric_limits<double>::infinity();
      Gq.zeros(L.n_rows, L.n_cols);
      return;
    }

    double logDetD = accu(log(dM));
    f = -(logDetM - logDetD) / 4.0;
    Gq = -(L % (L2 * (Minv - diagmat(1.0 / dM))));
  }
};

// Jennrich-Bentler bifactor (biquartimin) criterion (GPArotation::vgQ.bifactor). The first
// factor is treated as the general factor and is exempt from the penalty: with the squared
// group-factor loadings L2 = L[, -1]^2 and their per-row sums r_i = sum_j L2_ij, the criterion
// f = sum_i r_i^2 - sum_{i,j} L2_ij^2 = sum_i sum_{j != j'} L2_ij L2_ij' is the sum of the
// between-group-factor cross-products of squared loadings; it is minimized when each variable
// loads on the general factor plus at most one group factor (bifactor simple structure). The
// gradient over the group columns is 4 L[, -1] .* (r_i - L2) (with r_i broadcast across the
// group columns) and the general (first) column of the gradient is zero. The criterion is a
// polynomial in L, so it is finite for any finite L and needs no degenerate-state guard. With a
// single group factor (two factors) the criterion is identically zero, matching GPArotation.
//
// The general factor defaults to the first column (the rotation's own convention). The analytic
// standard errors construct the criterion with an explicit `general_col` because the reported
// solution has been factor-reordered by `.reflect_and_order` (sum-of-squared loadings), which can
// move the general factor out of the first column; exempting the right column there lets the
// warm-started re-rotation reproduce the reported loadings.
struct BifactorCriterion : public RotationCriterion {
  arma::uword general_col_;
  explicit BifactorCriterion(arma::uword general_col = 0) : general_col_(general_col) {}

  void eval(const arma::mat& L, double& f, arma::mat& Gq) const override {
    const arma::uword p = L.n_rows;
    const arma::uword k = L.n_cols;
    Gq.set_size(p, k);

    if (general_col_ == 0) {
      // Fast contiguous path (the rotation hot path): the general factor is the first column.
      arma::mat Lg = L.cols(1, k - 1);
      arma::mat L2 = square(Lg);
      arma::vec rowS = sum(L2, 1);  // per-row sum of the group-factor squared loadings
      f = accu(square(rowS)) - accu(square(L2));
      // group-column gradient 4 L[, -1] .* (r_i - L2); the general (first) column is zero
      arma::mat Gt = -L2;
      Gt.each_col() += rowS;
      Gt = 4.0 * (Lg % Gt);
      // Only the general column needs zeroing; the group block is fully overwritten by Gt, so
      // zero-filling the whole gradient first would waste a write over p * (k - 1) entries on every
      // evaluation (this is the innermost kernel of the multi-start solver).
      Gq.col(0).zeros();
      Gq.cols(1, k - 1) = Gt;
      return;
    }

    // General path (analytic standard errors only): the group block is every column except the
    // general factor, which sits at general_col_.
    arma::uvec group_idx(k - 1);
    arma::uword j = 0;
    for (arma::uword c = 0; c < k; ++c) {
      if (c != general_col_) group_idx(j++) = c;
    }
    arma::mat Lg = L.cols(group_idx);
    arma::mat L2 = square(Lg);
    arma::vec rowS = sum(L2, 1);
    f = accu(square(rowS)) - accu(square(L2));
    arma::mat Gt = -L2;
    Gt.each_col() += rowS;
    Gt = 4.0 * (Lg % Gt);
    Gq.col(general_col_).zeros();
    Gq.cols(group_idx) = Gt;
  }
};

// Simplimax criterion (GPArotation::vgQ.simplimax). With the k smallest squared loadings
// flagged by a 0/1 indicator matrix Imat -- k ("the number of close-to-zero loadings") a
// tuning parameter -- the criterion f = sum of those k smallest squared loadings is minimized
// when the k smallest loadings are driven toward zero, concentrating each variable's weight on
// a few factors (simple structure). The gradient is Gq = 2 Imat .* L. The indicator set is
// data-dependent and recomputed at every evaluation -- as the rotated loadings move, which k
// entries are smallest changes -- so the criterion is only piecewise smooth (its gradient jumps
// as loadings cross the kth-smallest threshold). The k smallest are selected with the same
// tie-breaking as R's order(L2)[1:k] (ascending squared loading; ties broken by ascending
// column-major index), which selects exactly k entries; a boundary tie makes GPArotation's
// sign(L^2 <= sort(L^2)[k]) select more than k, so the two can differ on exactly-tied loadings.
// Selecting exactly k follows Kiers (1994). The criterion is finite for any finite L, so no
// degenerate-state guard is needed.
struct SimplimaxCriterion : public RotationCriterion {
  arma::uword k_;
  // Reusable index scratch for the partial_sort below, sized once and reused across the many
  // eval() calls of a single rotation (the engine runs serially), avoiding a per-evaluation
  // heap allocation on this hot path.
  mutable std::vector<arma::uword> idx_;
  explicit SimplimaxCriterion(arma::uword k) : k_(k) {}

  void eval(const arma::mat& L, double& f, arma::mat& Gq) const override {
    arma::vec l2 = vectorise(square(L));  // column-major, matching R's order(L2)
    const arma::uword n = l2.n_elem;
    // k is validated to [1, n] at the entry point; clamp defensively so the partial_sort middle
    // iterator can never run past the end if the criterion is ever constructed against a sub-block.
    const arma::uword kk = std::min(k_, n);

    // Indices of the k smallest squared loadings. partial_sort places only the first kk in order;
    // the comparator breaks ties by ascending index to mirror R's stable order(L2)[1:k].
    idx_.resize(n);
    for (arma::uword i = 0; i < n; ++i) {
      idx_[i] = i;
    }
    std::partial_sort(idx_.begin(), idx_.begin() + kk, idx_.end(),
                      [&l2](arma::uword a, arma::uword b) {
                        return l2[a] < l2[b] || (l2[a] == l2[b] && a < b);
                      });

    // f = sum of the kk smallest squared loadings; the gradient 2*Imat*L equals 2*L on those
    // entries and zero elsewhere, so write it directly (arma linear indexing is column-major,
    // matching the vectorise above) rather than forming the indicator matrix and an elementwise
    // product.
    Gq.zeros(L.n_rows, L.n_cols);
    double fval = 0.0;
    for (arma::uword i = 0; i < kk; ++i) {
      const arma::uword j = idx_[i];
      fval += l2[j];
      Gq(j) = 2.0 * L(j);
    }
    f = fval;
  }
};


// Retract X back onto the orthogonal group via its polar factor U V' from the SVD,
// the Stiefel retraction used by GPForth. Returns false (leaving Tout untouched) if
// the decomposition fails or yields a non-finite matrix, so the candidate is rejected.
static bool retract_orth(const arma::mat& X, arma::mat& Tout) {
  arma::mat U, V;
  arma::vec s;
  if (!arma::svd(U, s, V, X)) {
    return false;
  }
  Tout = U * V.t();
  return all_finite_cpp(Tout);
}

// Orthogonal rotation manifold for the shared gradient-projection engine. The
// transformation T lives on the Stiefel manifold (T'T = I) and the rotated loadings
// are L = A %*% T. The criterion supplies the objective f and its gradient Gq = dQ/dL;
// this maps Gq to the gradient with respect to T (G = A' Gq), projects it onto the
// tangent space (Gp = G - T (T'G + G'T)/2), and reports its Frobenius norm. Candidates
// are retracted back onto the orthogonal group by the polar (SVD) projection. A
// non-finite criterion or gradient yields an infinite (invalid) objective. The multi-start
// screen, which reads the objective alone, takes it from screen_objective() and so skips
// the gradient mapping and the tangent projection.
struct OrthCriterionManifold {
  const arma::mat& A;
  const RotationCriterion& crit;

  // The criterion at T, shared by compute() and the objective-only screen below so both
  // read one source. Returns false for a state the manifold rejects before any gradient
  // mapping (non-finite loadings, criterion or criterion gradient). The rotated loadings
  // stay local: the orthogonal gradient mapping reads A and Gq, not L.
  bool value_at(const arma::mat& Tmat, double& f, arma::mat& Gq) const {
    arma::mat L = A * Tmat;
    if (!all_finite_cpp(L)) {
      return false;
    }
    crit.eval(L, f, Gq);
    return std::isfinite(f) && all_finite_cpp(Gq);
  }

  GpfState compute(const arma::mat& Tmat) const {
    double f;
    arma::mat Gq;
    if (!value_at(Tmat, f, Gq)) {
      return gpf_invalid_state();
    }

    arma::mat G = A.t() * Gq;
    arma::mat M = Tmat.t() * G;
    arma::mat S = (M + M.t()) / 2.0;
    arma::mat Gp = G - Tmat * S;
    double s = norm(vectorise(Gp), 2);

    GpfState out;
    out.f = f;
    out.s = s;
    out.Gp = Gp;
    out.valid = std::isfinite(s) && all_finite_cpp(G) && all_finite_cpp(Gp);

    if (!out.valid) {
      return gpf_invalid_state();
    }
    return out;
  }

  double screen_objective(const arma::mat& Tmat) const {
    double f = gpf_invalid_objective();
    arma::mat Gq;
    if (!value_at(Tmat, f, Gq)) {
      return gpf_invalid_objective();
    }
    return f;
  }

  bool retract(const arma::mat& X, arma::mat& T_out) const {
    return retract_orth(X, T_out);
  }

  // Orthogonal starts (identity and random_orthogonal_start_cpp) are already on the
  // manifold, so the start needs no further conditioning.
  arma::mat normalize_start(const arma::mat& Tmat) const {
    return Tmat;
  }
};

// Oblique rotation manifold for the shared gradient-projection engine. The transformation
// T lives on the column-normalized manifold diag(T'T) = 1 and the rotated loadings are
// L = A %*% solve(t(T)). The criterion supplies the objective f and its gradient
// Gq = dQ/dL; this maps Gq to the gradient with respect to T
// (G = -t(t(L) %*% Gq %*% solve(T))), projects it onto the tangent space
// (Gp = G - T diag(colSums(T .* G))), and reports its Frobenius norm. Candidates are
// retracted back onto the manifold by column normalization. This mirrors GPArotation's
// GPFoblq(); only the criterion functor changes between rotation methods. A non-invertible
// T or a non-finite criterion/gradient yields an infinite (invalid) objective. The
// multi-start screen, which reads the objective alone, takes it from screen_objective()
// and so skips the gradient mapping and the tangent projection.
struct OblqCriterionManifold {
  const arma::mat& A;
  const RotationCriterion& crit;

  // Inverse transformation, rotated loadings and the criterion at T, shared by compute()
  // and the objective-only screen below so both read one source. Returns false for a state
  // the manifold rejects before any gradient mapping (a non-invertible T, non-finite
  // loadings, criterion or criterion gradient).
  bool value_at(const arma::mat& Tmat, arma::mat& invT, arma::mat& L, double& f,
                arma::mat& Gq) const {
    if (!inverse_checked_cpp(Tmat, invT, oblq_min_sigma)) {
      return false;
    }
    L = A * invT.t();
    if (!all_finite_cpp(L)) {
      return false;
    }
    crit.eval(L, f, Gq);
    return std::isfinite(f) && all_finite_cpp(Gq);
  }

  GpfState compute(const arma::mat& Tmat) const {
    arma::mat invT;
    arma::mat L;
    double f;
    arma::mat Gq;
    if (!value_at(Tmat, invT, L, f, Gq)) {
      return gpf_invalid_state();
    }

    arma::mat G = -(L.t() * Gq * invT).t();
    arma::mat Gp;
    double s;
    oblique_tangent_project(Tmat, G, Gp, s);

    GpfState out;
    out.f = f;
    out.s = s;
    out.Gp = Gp;
    out.valid = std::isfinite(s) && all_finite_cpp(G) && all_finite_cpp(Gp);

    if (!out.valid) {
      return gpf_invalid_state();
    }
    return out;
  }

  double screen_objective(const arma::mat& Tmat) const {
    arma::mat invT;
    arma::mat L;
    double f = gpf_invalid_objective();
    arma::mat Gq;
    if (!value_at(Tmat, invT, L, f, Gq)) {
      return gpf_invalid_objective();
    }
    return f;
  }

  // The column-normalization projection always returns a candidate on the manifold; a
  // singular candidate is rejected later by the inverse check in compute().
  bool retract(const arma::mat& X, arma::mat& T_out) const {
    T_out = normalize_cols_cpp(X);
    return true;
  }

  arma::mat normalize_start(const arma::mat& Tmat) const {
    return normalize_cols_cpp(Tmat);
  }
};

// Kaiser row weights for a single loading matrix: each row weight is its loading
// norm sqrt(rowSumSq). Dividing the rows by these weights before rotation and
// restoring afterwards is Kaiser normalization. A zero/non-finite row norm (a
// zero-communality row) is floored to 1 so the row is left unchanged rather than
// divided by zero -- the same null-row guard the varimax and Procrustes paths use.
static arma::vec kaiser_weights(const arma::mat& A) {
  arma::vec W = sqrt(sum(square(A), 1));
  for (arma::uword i = 0; i < W.n_elem; ++i) {
    if (!std::isfinite(W(i)) || W(i) < 1e-15) {
      W(i) = 1.0;
    }
  }
  return W;
}

// Shared body of the orthogonal criterion rotation entry points (Crawford-Ferguson, geomin,
// Bentler, bifactor). The entries differ only in their criterion-specific scalar validation and
// the criterion functor they construct; the optimization is otherwise identical, so it lives
// here once. Kaiser normalization scales the rows before rotation, and because the weights act
// on rows while the rotation right-multiplies they cancel in the final loadings, so the rotated
// loadings are reported as L %*% Th (the documented reproduction identity). `crit` must be a
// RotationCriterion subclass.
template <typename Crit>
static Rcpp::List make_orth_entry(const arma::mat& L, const Crit& crit,
                                  double eps, bool normalize, int random_starts,
                                  int maxit, int max_line_search, double step0,
                                  int screen_keep, int triage_maxit,
                                  double triage_improve_tol) {
  const arma::uword k = L.n_cols;

  arma::mat A_work = L;
  arma::vec W;
  if (normalize) {
    W = kaiser_weights(L);
    A_work.each_col() /= W;
  }

  OrthCriterionManifold manifold{A_work, crit};
  arma::mat T_primary(k, k, fill::eye);

  GpfSummary summary = run_gpf_multistart(
    manifold, T_primary, eps, maxit, max_line_search, step0,
    random_starts, screen_keep, triage_maxit, triage_improve_tol
  );
  const GpfFit& best_fit = summary.best_fit;

  arma::mat loadings = L * best_fit.T;

  return Rcpp::List::create(
    Rcpp::Named("loadings") = loadings,
    Rcpp::Named("Th") = best_fit.T,
    Rcpp::Named("value") = best_fit.value,
    Rcpp::Named("convergence") = best_fit.convergence,
    Rcpp::Named("valid") = best_fit.valid,
    Rcpp::Named("all_values") = Rcpp::wrap(summary.all_values),
    Rcpp::Named("all_converged") = Rcpp::wrap(summary.all_converged)
  );
}

// Shared body of the oblique criterion rotation entry points (oblimin, geomin, Bentler,
// bifactor, simplimax). As with make_orth_entry the entries differ only in their
// criterion-specific validation and functor. Here the rotation acts through solve(t(T)), so the
// Kaiser weights again cancel and the rotated loadings reproduce L %*% t(solve(Th)); the rotated
// loadings and the factor correlations Phi are reconstructed from the winning transformation by
// finalize_oblique() (shared with the Procrustes oblique entry point). `full_multistart` selects
// the full-multistart strategy that the strongly multimodal simplimax criterion needs;
// `allow_stall_convergence` separately enables its piecewise-smooth objective-stall rule. Both
// default off for the smooth criteria.
template <typename Crit>
static Rcpp::List make_oblq_entry(const arma::mat& L, const Crit& crit,
                                  double eps, bool normalize, int random_starts,
                                  int maxit, int max_line_search, double step0,
                                  int screen_keep, int triage_maxit,
                                  double triage_improve_tol,
                                  bool full_multistart = false,
                                  bool allow_stall_convergence = false) {
  const arma::uword k = L.n_cols;

  arma::mat A_work = L;
  arma::vec W;
  if (normalize) {
    W = kaiser_weights(L);
    A_work.each_col() /= W;
  }

  OblqCriterionManifold manifold{A_work, crit};
  arma::mat T_primary(k, k, fill::eye);

  GpfSummary summary = run_gpf_multistart(
    manifold, T_primary, eps, maxit, max_line_search, step0,
    random_starts, screen_keep, triage_maxit, triage_improve_tol,
    full_multistart, allow_stall_convergence
  );
  const GpfFit& best_fit = summary.best_fit;

  arma::mat loadings;
  arma::mat Phi;
  finalize_oblique(best_fit, A_work, W, normalize, loadings, Phi);

  return Rcpp::List::create(
    Rcpp::Named("loadings") = loadings,
    Rcpp::Named("Th") = best_fit.T,
    Rcpp::Named("Phi") = Phi,
    Rcpp::Named("value") = best_fit.value,
    Rcpp::Named("convergence") = best_fit.convergence,
    Rcpp::Named("valid") = best_fit.valid,
    Rcpp::Named("all_values") = Rcpp::wrap(summary.all_values),
    Rcpp::Named("all_converged") = Rcpp::wrap(summary.all_converged)
  );
}

//' Orthogonal Crawford-Ferguson factor rotation
//'
//' Rotate a loading matrix orthogonally under the Crawford-Ferguson criterion using a
//' gradient-projection optimizer along the orthogonal (Stiefel) manifold.
//'
//' The criterion value `f` and its gradient `dQ/dL` at the rotated loadings
//' `L = A %*% T` define the search; the engine maps the gradient to the orthogonal
//' transformation `T`, projects it onto the tangent space, performs a
//' non-monotone line search, and retracts back onto the orthogonal group via a
//' polar (singular value) projection. `kappa = 0` is the quartimax criterion and
//' `kappa = ncol(A) / (2 * nrow(A))` is the equamax criterion.
//'
//' Additional random orthogonal starts may be requested. To bound runtime the solver
//' screens each random start by its objective, runs a short triage optimization on the
//' best-screened starts, and fully optimizes only those that improve on the current
//' incumbent by at least `triage_improve_tol`.
//'
//' @param L Numeric matrix. The unrotated loading matrix (variables by factors).
//' @param kappa Numeric scalar in `[0, 1]`. The Crawford-Ferguson parameter.
//' @param eps Numeric scalar. Convergence tolerance for the projected-gradient norm.
//' @param normalize Logical scalar. If `TRUE`, apply Kaiser normalization before
//'   rotation and reverse it afterwards.
//' @param random_starts Integer scalar. Number of additional random orthogonal starts.
//' @param maxit Integer scalar. Maximum number of projected-gradient updates.
//' @param max_line_search Integer scalar. Maximum number of step-halving attempts after
//'   the initial trial step in each line-search phase.
//' @param step0 Numeric scalar. Initial step size used in the projected-gradient update.
//' @param screen_keep Integer scalar. Number of screened random starts retained for
//'   triage optimization.
//' @param triage_maxit Integer scalar. Number of short optimization iterations used in
//'   the triage stage.
//' @param triage_improve_tol Numeric scalar. Relative improvement required for a triaged
//'   start to be promoted to full optimization.
//'
//' @returns A named list with the rotated loadings, the orthogonal rotation matrix `Th`
//'   (with `L %*% Th` reproducing the rotated loadings), the attained criterion value, and
//'   the convergence and validity flags. The list additionally reports the
//'   criterion value reached at each optimized start in `all_values`, with a per-start convergence flag in `all_converged`.
//'
//' @references
//' Bernaards, C. A., & Jennrich, R. I. (2005). Gradient projection algorithms and
//' software for arbitrary rotation criteria in factor analysis. *Educational and
//' Psychological Measurement*, 65, 676-696.
//'
//' Crawford, C. B., & Ferguson, G. A. (1970). A general rotation criterion and its use
//' in orthogonal rotation. *Psychometrika*, 35, 321-332.
//'
//' @keywords internal
// [[Rcpp::export(.rotate_cf_orth)]]
Rcpp::List rotate_cf_orth(const arma::mat& L,
                          double kappa,
                          double eps = 1e-5,
                          bool normalize = true,
                          int random_starts = 0,
                          int maxit = 1000,
                          int max_line_search = 10,
                          double step0 = 1.0,
                          int screen_keep = 5,
                          int triage_maxit = 25,
                          double triage_improve_tol = 0.0) {
  validate_gpf_input(L, "Orthogonal");
  if (!is_valid_scalar_cpp(kappa) || kappa < 0.0 || kappa > 1.0) {
    Rcpp::stop("kappa must be a finite scalar in [0, 1].");
  }
  validate_gpf_scalars(eps, maxit, max_line_search, step0, random_starts,
                       screen_keep, triage_maxit, triage_improve_tol);

  CfCriterion crit(kappa);
  return make_orth_entry(L, crit, eps, normalize, random_starts, maxit,
                         max_line_search, step0, screen_keep, triage_maxit,
                         triage_improve_tol);
}

//' Oblique oblimin factor rotation
//'
//' Rotate a loading matrix obliquely under the oblimin criterion using a
//' gradient-projection optimizer along the oblique (column-normalized) manifold.
//'
//' The criterion value `f` and its gradient `dQ/dL` at the rotated loadings
//' `L = A %*% solve(t(T))` define the search; the engine maps the gradient to the
//' transformation `T` on the manifold `diag(t(T) %*% T) = 1`, projects it onto the tangent
//' space, performs a non-monotone line search, and retracts back onto the manifold
//' by column normalization. `gam = 0` is the quartimin criterion.
//'
//' Additional random starts may be requested. To bound runtime the solver screens each
//' random start by its objective, runs a short triage optimization on the best-screened
//' starts, and fully optimizes only those that improve on the current incumbent by at
//' least `triage_improve_tol`.
//'
//' @inheritParams .rotate_cf_orth
//' @param gam Numeric scalar. The oblimin parameter; `gam = 0` is the quartimin criterion
//'   and the recommended setting. Larger values increasingly reward correlated factors and
//'   can drive the solution toward factor collapse, where the rotation may also fail to
//'   converge; inspect `Phi` before interpreting a fit with `gam > 0`.
//'
//' @returns A named list with the rotated loadings, the transformation matrix `Th`
//'   (with `L %*% t(solve(Th))` reproducing the rotated loadings), the factor correlation
//'   matrix `Phi` (`t(Th) %*% Th`), the attained criterion value, and the convergence and
//'   validity flags. The list additionally reports the criterion value reached
//'   at each optimized start in `all_values`, with a per-start convergence flag in `all_converged`.
//'
//' @references
//' Bernaards, C. A., & Jennrich, R. I. (2005). Gradient projection algorithms and
//' software for arbitrary rotation criteria in factor analysis. *Educational and
//' Psychological Measurement*, 65, 676-696.
//'
//' Jennrich, R. I., & Sampson, P. F. (1966). Rotation for simple loadings.
//' *Psychometrika*, 31, 313-323.
//'
//' @keywords internal
// [[Rcpp::export(.rotate_oblimin)]]
Rcpp::List rotate_oblimin(const arma::mat& L,
                          double gam = 0.0,
                          double eps = 1e-5,
                          bool normalize = true,
                          int random_starts = 0,
                          int maxit = 1000,
                          int max_line_search = 10,
                          double step0 = 1.0,
                          int screen_keep = 5,
                          int triage_maxit = 25,
                          double triage_improve_tol = 0.0) {
  validate_gpf_input(L, "Oblique");
  if (!is_valid_scalar_cpp(gam)) {
    Rcpp::stop("gam must be a finite scalar.");
  }
  validate_gpf_scalars(eps, maxit, max_line_search, step0, random_starts,
                       screen_keep, triage_maxit, triage_improve_tol);

  ObliminCriterion crit(gam);
  return make_oblq_entry(L, crit, eps, normalize, random_starts, maxit,
                         max_line_search, step0, screen_keep, triage_maxit,
                         triage_improve_tol);
}

//' Orthogonal geomin factor rotation
//'
//' Rotate a loading matrix orthogonally under the geomin criterion using a
//' gradient-projection optimizer along the orthogonal (Stiefel) manifold.
//'
//' The criterion value `f` and its gradient `dQ/dL` at the rotated loadings
//' `L = A %*% T` define the search; the engine maps the gradient to the orthogonal
//' transformation `T`, projects it onto the tangent space, performs a non-monotone
//' line search, and retracts back onto the orthogonal group via a polar (singular value)
//' projection. The geomin criterion sums the per-variable geometric mean of the squared
//' loadings offset by `delta`; it is prone to local minima, so additional random starts are
//' recommended.
//'
//' Additional random orthogonal starts may be requested. To bound runtime the solver screens
//' each random start by its objective, runs a short triage optimization on the best-screened
//' starts, and fully optimizes only those that improve on the current incumbent by at least
//' `triage_improve_tol`.
//'
//' @inheritParams .rotate_cf_orth
//' @param delta Numeric scalar. The geomin offset added to the squared loadings; must be a
//'   positive finite scalar. `delta = 0.01` is the usual default.
//'
//' @returns A named list with the rotated loadings, the orthogonal rotation matrix `Th`
//'   (with `L %*% Th` reproducing the rotated loadings), the attained criterion value, and the
//'   convergence and validity flags. The list additionally reports the criterion
//'   value reached at each optimized start in `all_values`, with a per-start convergence flag in `all_converged`.
//'
//' @references
//' Bernaards, C. A., & Jennrich, R. I. (2005). Gradient projection algorithms and
//' software for arbitrary rotation criteria in factor analysis. *Educational and
//' Psychological Measurement*, 65, 676-696.
//'
//' Browne, M. W. (2001). An overview of analytic rotation in exploratory factor analysis.
//' *Multivariate Behavioral Research*, 36, 111-150.
//'
//' @keywords internal
// [[Rcpp::export(.rotate_geomin_orth)]]
Rcpp::List rotate_geomin_orth(const arma::mat& L,
                              double delta = 0.01,
                              double eps = 1e-5,
                              bool normalize = true,
                              int random_starts = 0,
                              int maxit = 1000,
                              int max_line_search = 10,
                              double step0 = 1.0,
                              int screen_keep = 5,
                              int triage_maxit = 25,
                              double triage_improve_tol = 0.0) {
  validate_gpf_input(L, "Orthogonal");
  if (!is_valid_scalar_cpp(delta) || delta <= 0.0) {
    Rcpp::stop("delta must be a positive finite scalar.");
  }
  validate_gpf_scalars(eps, maxit, max_line_search, step0, random_starts,
                       screen_keep, triage_maxit, triage_improve_tol);

  GeominCriterion crit(delta);
  return make_orth_entry(L, crit, eps, normalize, random_starts, maxit,
                         max_line_search, step0, screen_keep, triage_maxit,
                         triage_improve_tol);
}

//' Oblique geomin factor rotation
//'
//' Rotate a loading matrix obliquely under the geomin criterion using a gradient-projection
//' optimizer along the oblique (column-normalized) manifold.
//'
//' The criterion value `f` and its gradient `dQ/dL` at the rotated loadings
//' `L = A %*% solve(t(T))` define the search; the engine maps the gradient to the
//' transformation `T` on the manifold `diag(t(T) %*% T) = 1`, projects it onto the tangent
//' space, performs a non-monotone line search, and retracts back onto the manifold by
//' column normalization. The geomin criterion sums the per-variable geometric mean of the
//' squared loadings offset by `delta`; it is prone to local minima, so additional random
//' starts are recommended.
//'
//' Additional random starts may be requested. To bound runtime the solver screens each random
//' start by its objective, runs a short triage optimization on the best-screened starts, and
//' fully optimizes only those that improve on the current incumbent by at least
//' `triage_improve_tol`.
//'
//' @inheritParams .rotate_cf_orth
//' @param delta Numeric scalar. The geomin offset added to the squared loadings; must be a
//'   positive finite scalar. `delta = 0.01` is the usual default.
//'
//' @returns A named list with the rotated loadings, the transformation matrix `Th`
//'   (with `L %*% t(solve(Th))` reproducing the rotated loadings), the factor correlation
//'   matrix `Phi` (`t(Th) %*% Th`), the attained criterion value, and the convergence and
//'   validity flags. The list additionally reports the criterion value reached
//'   at each optimized start in `all_values`, with a per-start convergence flag in `all_converged`.
//'
//' @references
//' Bernaards, C. A., & Jennrich, R. I. (2005). Gradient projection algorithms and
//' software for arbitrary rotation criteria in factor analysis. *Educational and
//' Psychological Measurement*, 65, 676-696.
//'
//' Browne, M. W. (2001). An overview of analytic rotation in exploratory factor analysis.
//' *Multivariate Behavioral Research*, 36, 111-150.
//'
//' @keywords internal
// [[Rcpp::export(.rotate_geomin_oblq)]]
Rcpp::List rotate_geomin_oblq(const arma::mat& L,
                              double delta = 0.01,
                              double eps = 1e-5,
                              bool normalize = true,
                              int random_starts = 0,
                              int maxit = 1000,
                              int max_line_search = 10,
                              double step0 = 1.0,
                              int screen_keep = 5,
                              int triage_maxit = 25,
                              double triage_improve_tol = 0.0) {
  validate_gpf_input(L, "Oblique");
  if (!is_valid_scalar_cpp(delta) || delta <= 0.0) {
    Rcpp::stop("delta must be a positive finite scalar.");
  }
  validate_gpf_scalars(eps, maxit, max_line_search, step0, random_starts,
                       screen_keep, triage_maxit, triage_improve_tol);

  GeominCriterion crit(delta);
  return make_oblq_entry(L, crit, eps, normalize, random_starts, maxit,
                         max_line_search, step0, screen_keep, triage_maxit,
                         triage_improve_tol);
}

//' Orthogonal Bentler factor rotation
//'
//' Rotate a loading matrix orthogonally under Bentler's invariant pattern simplicity criterion
//' using a gradient-projection optimizer along the orthogonal (Stiefel) manifold.
//'
//' The criterion value `f` and its gradient `dQ/dL` at the rotated loadings
//' `L = A %*% T` define the search; the engine maps the gradient to the orthogonal
//' transformation `T`, projects it onto the tangent space, performs a non-monotone line
//' search, and retracts back onto the orthogonal group via a polar (singular value) projection.
//' The Bentler criterion measures the departure of the cross-products of squared loadings from a
//' diagonal pattern; it is prone to local minima, so additional random starts are recommended.
//'
//' Additional random orthogonal starts may be requested. To bound runtime the solver screens
//' each random start by its objective, runs a short triage optimization on the best-screened
//' starts, and fully optimizes only those that improve on the current incumbent by at least
//' `triage_improve_tol`.
//'
//' @inheritParams .rotate_cf_orth
//'
//' @returns A named list with the rotated loadings, the orthogonal rotation matrix `Th`
//'   (with `L %*% Th` reproducing the rotated loadings), the attained criterion value, and the
//'   convergence and validity flags. The list additionally reports the criterion
//'   value reached at each optimized start in `all_values`, with a per-start convergence flag in `all_converged`.
//'
//' @references
//' Bentler, P. M. (1977). Factor simplicity index and transformations. *Psychometrika*, 42,
//' 277-295.
//'
//' Bernaards, C. A., & Jennrich, R. I. (2005). Gradient projection algorithms and
//' software for arbitrary rotation criteria in factor analysis. *Educational and
//' Psychological Measurement*, 65, 676-696.
//'
//' @keywords internal
// [[Rcpp::export(.rotate_bentler_orth)]]
Rcpp::List rotate_bentler_orth(const arma::mat& L,
                               double eps = 1e-5,
                               bool normalize = true,
                               int random_starts = 0,
                               int maxit = 1000,
                               int max_line_search = 10,
                               double step0 = 1.0,
                               int screen_keep = 5,
                               int triage_maxit = 25,
                               double triage_improve_tol = 0.0) {
  validate_gpf_input(L, "Orthogonal");
  validate_gpf_scalars(eps, maxit, max_line_search, step0, random_starts,
                       screen_keep, triage_maxit, triage_improve_tol);

  BentlerCriterion crit;
  return make_orth_entry(L, crit, eps, normalize, random_starts, maxit,
                         max_line_search, step0, screen_keep, triage_maxit,
                         triage_improve_tol);
}

//' Oblique Bentler factor rotation
//'
//' Rotate a loading matrix obliquely under Bentler's invariant pattern simplicity criterion
//' using a gradient-projection optimizer along the oblique (column-normalized) manifold.
//'
//' The criterion value `f` and its gradient `dQ/dL` at the rotated loadings
//' `L = A %*% solve(t(T))` define the search; the engine maps the gradient to the
//' transformation `T` on the manifold `diag(t(T) %*% T) = 1`, projects it onto the tangent
//' space, performs a non-monotone line search, and retracts back onto the manifold by
//' column normalization. The Bentler criterion measures the departure of the cross-products of
//' squared loadings from a diagonal pattern; it is prone to local minima, so additional random
//' starts are recommended.
//'
//' Additional random starts may be requested. To bound runtime the solver screens each random
//' start by its objective, runs a short triage optimization on the best-screened starts, and
//' fully optimizes only those that improve on the current incumbent by at least
//' `triage_improve_tol`.
//'
//' @inheritParams .rotate_cf_orth
//'
//' @returns A named list with the rotated loadings, the transformation matrix `Th`
//'   (with `L %*% t(solve(Th))` reproducing the rotated loadings), the factor correlation
//'   matrix `Phi` (`t(Th) %*% Th`), the attained criterion value, and the convergence and
//'   validity flags. The list additionally reports the criterion value reached
//'   at each optimized start in `all_values`, with a per-start convergence flag in `all_converged`.
//'
//' @references
//' Bentler, P. M. (1977). Factor simplicity index and transformations. *Psychometrika*, 42,
//' 277-295.
//'
//' Bernaards, C. A., & Jennrich, R. I. (2005). Gradient projection algorithms and
//' software for arbitrary rotation criteria in factor analysis. *Educational and
//' Psychological Measurement*, 65, 676-696.
//'
//' @keywords internal
// [[Rcpp::export(.rotate_bentler_oblq)]]
Rcpp::List rotate_bentler_oblq(const arma::mat& L,
                               double eps = 1e-5,
                               bool normalize = true,
                               int random_starts = 0,
                               int maxit = 1000,
                               int max_line_search = 10,
                               double step0 = 1.0,
                               int screen_keep = 5,
                               int triage_maxit = 25,
                               double triage_improve_tol = 0.0) {
  validate_gpf_input(L, "Oblique");
  validate_gpf_scalars(eps, maxit, max_line_search, step0, random_starts,
                       screen_keep, triage_maxit, triage_improve_tol);

  BentlerCriterion crit;
  return make_oblq_entry(L, crit, eps, normalize, random_starts, maxit,
                         max_line_search, step0, screen_keep, triage_maxit,
                         triage_improve_tol);
}

//' Orthogonal bifactor factor rotation
//'
//' Rotate a loading matrix orthogonally under the Jennrich-Bentler bifactor criterion using a
//' gradient-projection optimizer along the orthogonal (Stiefel) manifold.
//'
//' The criterion value `f` and its gradient `dQ/dL` at the rotated loadings
//' `L = A %*% T` define the search; the engine maps the gradient to the orthogonal
//' transformation `T`, projects it onto the tangent space, performs a non-monotone line
//' search, and retracts back onto the orthogonal group via a polar (singular value) projection.
//' The first factor is treated as a general factor and is exempt from the penalty; the criterion
//' measures the between-group-factor cross-products of the squared loadings, so it is minimized
//' when each variable loads on the general factor plus at most one group factor. The criterion is
//' prone to local minima, so additional random starts are recommended.
//'
//' Additional random orthogonal starts may be requested. To bound runtime the solver screens
//' each random start by its objective, runs a short triage optimization on the best-screened
//' starts, and fully optimizes only those that improve on the current incumbent by at least
//' `triage_improve_tol`.
//'
//' @inheritParams .rotate_cf_orth
//'
//' @returns A named list with the rotated loadings, the orthogonal rotation matrix `Th`
//'   (with `L %*% Th` reproducing the rotated loadings), the attained criterion value, and the
//'   convergence and validity flags. The list additionally reports the criterion
//'   value reached at each optimized start in `all_values`, with a per-start convergence flag in `all_converged`.
//'
//' @references
//' Bernaards, C. A., & Jennrich, R. I. (2005). Gradient projection algorithms and
//' software for arbitrary rotation criteria in factor analysis. *Educational and
//' Psychological Measurement*, 65, 676-696.
//'
//' Jennrich, R. I., & Bentler, P. M. (2011). Exploratory bi-factor analysis. *Psychometrika*,
//' 76, 537-549.
//'
//' @keywords internal
// [[Rcpp::export(.rotate_bifactor_orth)]]
Rcpp::List rotate_bifactor_orth(const arma::mat& L,
                                double eps = 1e-5,
                                bool normalize = true,
                                int random_starts = 0,
                                int maxit = 1000,
                                int max_line_search = 10,
                                double step0 = 1.0,
                                int screen_keep = 5,
                                int triage_maxit = 25,
                                double triage_improve_tol = 0.0) {
  validate_gpf_input(L, "Orthogonal");
  validate_gpf_scalars(eps, maxit, max_line_search, step0, random_starts,
                       screen_keep, triage_maxit, triage_improve_tol);

  BifactorCriterion crit;
  return make_orth_entry(L, crit, eps, normalize, random_starts, maxit,
                         max_line_search, step0, screen_keep, triage_maxit,
                         triage_improve_tol);
}

//' Oblique bifactor factor rotation
//'
//' Rotate a loading matrix obliquely under the Jennrich-Bentler bifactor criterion using a
//' gradient-projection optimizer along the oblique (column-normalized) manifold.
//'
//' The criterion value `f` and its gradient `dQ/dL` at the rotated loadings
//' `L = A %*% solve(t(T))` define the search; the engine maps the gradient to the
//' transformation `T` on the manifold `diag(t(T) %*% T) = 1`, projects it onto the tangent
//' space, performs a non-monotone line search, and retracts back onto the manifold by
//' column normalization. The first factor is treated as a general factor and is exempt from the
//' penalty; the criterion measures the between-group-factor cross-products of the squared
//' loadings, so it is minimized when each variable loads on the general factor plus at most one
//' group factor. The criterion is prone to local minima, so additional random starts are
//' recommended.
//'
//' Additional random starts may be requested. To bound runtime the solver screens each random
//' start by its objective, runs a short triage optimization on the best-screened starts, and
//' fully optimizes only those that improve on the current incumbent by at least
//' `triage_improve_tol`.
//'
//' @inheritParams .rotate_cf_orth
//'
//' @returns A named list with the rotated loadings, the transformation matrix `Th`
//'   (with `L %*% t(solve(Th))` reproducing the rotated loadings), the factor correlation
//'   matrix `Phi` (`t(Th) %*% Th`), the attained criterion value, and the convergence and
//'   validity flags. The list additionally reports the criterion value reached
//'   at each optimized start in `all_values`, with a per-start convergence flag in `all_converged`.
//'
//' @references
//' Bernaards, C. A., & Jennrich, R. I. (2005). Gradient projection algorithms and
//' software for arbitrary rotation criteria in factor analysis. *Educational and
//' Psychological Measurement*, 65, 676-696.
//'
//' Jennrich, R. I., & Bentler, P. M. (2011). Exploratory bi-factor analysis. *Psychometrika*,
//' 76, 537-549.
//'
//' @keywords internal
// [[Rcpp::export(.rotate_bifactor_oblq)]]
Rcpp::List rotate_bifactor_oblq(const arma::mat& L,
                                double eps = 1e-5,
                                bool normalize = true,
                                int random_starts = 0,
                                int maxit = 1000,
                                int max_line_search = 10,
                                double step0 = 1.0,
                                int screen_keep = 5,
                                int triage_maxit = 25,
                                double triage_improve_tol = 0.0) {
  validate_gpf_input(L, "Oblique");
  validate_gpf_scalars(eps, maxit, max_line_search, step0, random_starts,
                       screen_keep, triage_maxit, triage_improve_tol);

  BifactorCriterion crit;
  return make_oblq_entry(L, crit, eps, normalize, random_starts, maxit,
                         max_line_search, step0, screen_keep, triage_maxit,
                         triage_improve_tol);
}

//' Oblique simplimax factor rotation
//'
//' Rotate a loading matrix obliquely under the simplimax criterion using a gradient-projection
//' optimizer along the oblique (column-normalized) manifold.
//'
//' The criterion value `f` and its gradient `dQ/dL` at the rotated loadings
//' `L = A %*% solve(t(T))` define the search; the engine maps the gradient to the
//' transformation `T` on the manifold `diag(t(T) %*% T) = 1`, projects it onto the tangent
//' space, performs a non-monotone line search, and retracts back onto the manifold by column
//' normalization. The simplimax criterion sums the `k` smallest squared loadings, so it is
//' minimized when the `k` "close-to-zero" loadings are driven toward zero; the count `k` is a
//' tuning parameter. Because the set of `k` smallest loadings is reselected at every evaluation,
//' the criterion is only piecewise smooth: its gradient jumps as loadings cross the kth-smallest threshold, so the
//' line search accepts a step whenever it decreases the largest objective over a short window of
//' recent iterations (a non-monotone test; Grippo, Lampariello, & Lucidi, 1986), letting the
//' optimizer step across the kinks where a strictly monotone descent would stall.
//'
//' The criterion is strongly prone to local minima, so the solver fully optimizes the identity
//' start together with `random_starts` random orthogonal starts and keeps the solution with the
//' lowest criterion value. Fully optimizing every start -- rather than the screen-and-triage
//' strategy used for the smooth criteria, which assumes the rational start lies in the global
//' basin -- is the standard remedy for the local minima of complexity-based rotation criteria
//' (Kiers, 1994; Browne, 2001).
//'
//' @inheritParams .rotate_cf_orth
//' @param k Integer scalar. The number of "close-to-zero" loadings the criterion targets; must
//'   be in `[1, nrow(L) * ncol(L)]`. `k = nrow(L)` is the usual default.
//' @param eps Numeric scalar. Convergence tolerance for the projected-gradient norm. Because the
//'   simplimax criterion is only piecewise smooth, the projected gradient need not reach this
//'   tolerance at the optimum; convergence is then reported when the criterion value stalls (the
//'   non-monotone search described above), so `eps` mainly governs the smooth phases of the search.
//' @param random_starts Integer scalar. Number of random orthogonal starts fully optimized in
//'   addition to the identity start.
//' @param maxit Integer scalar. Maximum number of projected-gradient updates per start.
//'
//' @returns A named list with the rotated loadings, the transformation matrix `Th`
//'   (with `L %*% t(solve(Th))` reproducing the rotated loadings), the factor correlation
//'   matrix `Phi` (`t(Th) %*% Th`), the attained criterion value, and the convergence and
//'   validity flags. The list additionally reports the criterion value reached
//'   at each optimized start in `all_values`, with a per-start convergence flag in `all_converged`.
//'
//' @references
//' Bernaards, C. A., & Jennrich, R. I. (2005). Gradient projection algorithms and
//' software for arbitrary rotation criteria in factor analysis. *Educational and
//' Psychological Measurement*, 65, 676-696.
//'
//' Browne, M. W. (2001). An overview of analytic rotation in exploratory factor analysis.
//' *Multivariate Behavioral Research*, 36, 111-150.
//'
//' Grippo, L., Lampariello, F., & Lucidi, S. (1986). A nonmonotone line search technique for
//' Newton's method. *SIAM Journal on Numerical Analysis*, 23, 707-716.
//'
//' Kiers, H. A. L. (1994). Simplimax: Oblique rotation to an optimal target with simple
//' structure. *Psychometrika*, 59, 567-579.
//'
//' @keywords internal
// [[Rcpp::export(.rotate_simplimax_oblq)]]
Rcpp::List rotate_simplimax_oblq(const arma::mat& L,
                                 int k,
                                 double eps = 1e-5,
                                 bool normalize = true,
                                 int random_starts = 0,
                                 int maxit = 1000,
                                 int max_line_search = 10,
                                 double step0 = 1.0) {
  validate_gpf_input(L, "Oblique");
  if (k < 1 || static_cast<arma::uword>(k) > L.n_elem) {
    Rcpp::stop("k must be an integer in [1, nrow(L) * ncol(L)].");
  }

  // simplimax is strongly multimodal, so the engine's full-multistart mode fully optimizes every
  // start and keeps the lowest-criterion solution (Kiers, 1994; Browne, 2001), rather than the
  // screen-and-triage heuristic the smooth criteria use (its early-iteration objective screen is
  // uninformative for this criterion). The engine's non-monotone line search (Grippo, Lampariello,
  // & Lucidi, 1986) handles its piecewise-smoothness. The screen and triage controls are unused in
  // full-multistart mode.
  validate_gpf_scalars(eps, maxit, max_line_search, step0, random_starts, 0, 0, 0.0);

  SimplimaxCriterion crit(static_cast<arma::uword>(k));
  return make_oblq_entry(L, crit, eps, normalize, random_starts, maxit,
                         max_line_search, step0, /*screen_keep=*/0, /*triage_maxit=*/0,
                         /*triage_improve_tol=*/0.0, /*full_multistart=*/true,
                         /*allow_stall_convergence=*/true);
}

// Rotated solution of a single warm-started re-rotation (loadings, and Phi when oblique).
struct WarmRotateResult {
  arma::mat loadings;
  arma::mat Phi;
  bool valid;
};

// Warm-started single rotation from a supplied transformation `T_init`. Used by the analytic
// rotation-Jacobian standard errors (R/EFA.R `.se_information_rotated`), which propagate the
// unrotated-loading covariance through the rotation by finite-differencing the map
// A -> (rotated loadings, Phi) over the unrotated loadings A. Each difference must re-solve the
// rotation from the converged transformation rather than from a fresh multi-start, so the rotated
// optimum tracks the perturbation smoothly. This runs one gradient-projection solve from `T_init`
// (no random starts) to a tight tolerance and reconstructs the rotated loadings -- and, when
// oblique, the factor correlations Phi -- exactly as the public rotation entries do. `crit` is one
// of the file-local rotation criteria.
static WarmRotateResult warm_rotate_core(const arma::mat& L, const RotationCriterion& crit,
                                         const arma::mat& T_init, bool normalize, bool oblique) {
  // A tight-but-reachable convergence tolerance keeps the re-rotation accurate enough that the
  // finite difference of the rotated loadings is not swamped by the solver's own residual, while
  // still letting the projected-gradient stopping test fire (a tolerance below the gradient's
  // floating-point floor would never be met, forcing every solve to run the full maxit). Warm-
  // started at the optimum it converges in a handful of iterations, so a modest maxit caps the
  // cost of the rare ill-conditioned perturbation without affecting the result.
  const double eps = 1e-7;
  const int maxit = 100;
  const int max_line_search = 10;
  const double step0 = 1.0;

  arma::mat A_work = L;
  arma::vec W;
  if (normalize) {
    W = kaiser_weights(L);
    A_work.each_col() /= W;
  }

  WarmRotateResult res;
  if (oblique) {
    OblqCriterionManifold manifold{A_work, crit};
    GpfSummary summary = run_gpf_multistart(manifold, T_init, eps, maxit, max_line_search,
                                            step0, /*random_starts=*/0, /*screen_keep=*/0,
                                            /*triage_maxit=*/0, /*triage_improve_tol=*/0.0);
    finalize_oblique(summary.best_fit, A_work, W, normalize, res.loadings, res.Phi);
    res.valid = summary.best_fit.valid;
  } else {
    // Orthogonal: the rotated loadings reproduce L %*% T on the raw (un-normalized) loadings, so
    // the Kaiser weights enter only the criterion that selects T (matching make_orth_entry).
    OrthCriterionManifold manifold{A_work, crit};
    GpfSummary summary = run_gpf_multistart(manifold, T_init, eps, maxit, max_line_search,
                                            step0, /*random_starts=*/0, /*screen_keep=*/0,
                                            /*triage_maxit=*/0, /*triage_improve_tol=*/0.0);
    res.loadings = L * summary.best_fit.T;
    res.valid = summary.best_fit.valid;
  }
  return res;
}

// Assemble the rotation Jacobians d vec(L) / d vec(A) and (oblique) d vec(Phi) / d vec(A) by
// forward-differencing the warm-started re-rotation over the p*k entries of A, against the
// re-rotation at the unperturbed A. The whole p*k loop is done in C++ to avoid p*k round trips to
// R: each column re-solves the rotation once from `T_init`. A forward stencil (rather than a
// central one) halves the re-rotations at no measurable accuracy cost, because the re-rotation is
// smooth and solved to a tight tolerance (it is also the stencil lavaan's delta method uses).
// Returns the Jacobians, the re-rotated point estimate at the unperturbed A (for the caller's
// consistency check), and a validity flag that is false if the base or any perturbed re-rotation
// failed.
template <typename Crit>
static Rcpp::List se_jacobian_impl(const arma::mat& A, const Crit& crit,
                                   const arma::mat& T_init, bool normalize, bool oblique,
                                   double eps) {
  const arma::uword p = A.n_rows;
  const arma::uword k = A.n_cols;
  const arma::uword pk = p * k;

  WarmRotateResult base = warm_rotate_core(A, crit, T_init, normalize, oblique);

  arma::mat J_L(pk, pk, arma::fill::zeros);
  arma::mat J_Phi;
  if (oblique) {
    J_Phi.set_size(k * k, pk);
    J_Phi.zeros();
  }
  bool valid = base.valid;
  const arma::vec base_l = valid ? arma::vectorise(base.loadings) : arma::vec();
  const arma::vec base_phi = (valid && oblique) ? arma::vectorise(base.Phi) : arma::vec();

  // One reusable copy: perturb a single entry, re-rotate, then restore it for the next column,
  // rather than allocating a fresh p*k copy of A on every iteration. warm_rotate_core() copies its
  // input before mutating, so Ap is never altered by the call.
  arma::mat Ap = A;
  for (arma::uword col = 0; col < pk && valid; ++col) {
    Ap(col) += eps;
    WarmRotateResult rp = warm_rotate_core(Ap, crit, T_init, normalize, oblique);
    Ap(col) = A(col);
    if (!rp.valid) {
      valid = false;
      break;
    }
    J_L.col(col) = (arma::vectorise(rp.loadings) - base_l) / eps;
    if (oblique) {
      J_Phi.col(col) = (arma::vectorise(rp.Phi) - base_phi) / eps;
    }
  }

  Rcpp::List out = Rcpp::List::create(
    Rcpp::Named("J_L") = J_L,
    Rcpp::Named("base_loadings") = base.loadings,
    Rcpp::Named("valid") = valid);
  if (oblique) {
    out["J_Phi"] = J_Phi;
    out["base_Phi"] = base.Phi;
  }
  return out;
}

//' Rotation Jacobians for analytic rotation standard errors
//'
//' Forward-difference the warm-started re-rotation map `A -> (rotated loadings, Phi)` over the
//' unrotated loadings `A` to obtain the rotation Jacobians used by the analytic standard errors for
//' rotated loadings (`se = "information"` in [efa_fit()]). The full `nrow(A) * ncol(A)` finite-
//' difference loop runs in compiled code, re-solving the rotation from the converged transformation
//' `T_init` at each perturbation; the caller forms `J V J'` in R.
//'
//' @param A Numeric matrix. The unrotated loading matrix at the solution.
//' @param T_init Numeric matrix. The converged transformation that warm-starts each re-rotation.
//' @param method Character scalar. The criterion family: one of `"cf"`, `"oblimin"`, `"geomin"`,
//'   `"bentler"`, `"bifactor"`.
//' @param param Numeric scalar. The criterion's tuning argument (`kappa` for `"cf"`, `gam` for
//'   `"oblimin"`, `delta` for `"geomin"`); ignored for `"bentler"` and `"bifactor"`.
//' @param normalize Logical scalar. Apply Kaiser normalization before rotation and reverse it after.
//' @param oblique Logical scalar. Use the oblique (column-normalized) manifold; otherwise orthogonal.
//' @param eps Numeric scalar. The forward-difference step on the loadings.
//' @param general_col Integer scalar. For `method = "bifactor"`, the zero-based column holding the
//'   general factor in the (factor-reordered) reported solution; ignored by the other criteria.
//'
//' @returns A named list with the Jacobian `J_L` (`pk x pk`), the re-rotated `base_loadings`, a
//'   validity flag, and -- when oblique -- the Jacobian `J_Phi` (`k^2 x pk`) and `base_Phi`.
//'
//' @references
//' Jennrich, R. I. (1973). Standard errors for obliquely rotated factor loadings.
//' *Psychometrika*, 38, 593-604.
//'
//' @keywords internal
// [[Rcpp::export(.rotation_se_jacobian)]]
Rcpp::List rotation_se_jacobian(const arma::mat& A, const arma::mat& T_init,
                                std::string method, double param,
                                bool normalize, bool oblique, double eps,
                                int general_col = 0) {
  validate_gpf_input(A, oblique ? "Oblique" : "Orthogonal");
  const arma::uword k = A.n_cols;
  if (T_init.n_rows != k || T_init.n_cols != k || !all_finite_cpp(T_init)) {
    Rcpp::stop("T_init must be a finite k x k matrix.");
  }
  if (!is_valid_scalar_cpp(eps) || eps <= 0.0) {
    Rcpp::stop("eps must be a finite positive scalar.");
  }

  if (method == "cf") {
    if (!is_valid_scalar_cpp(param) || param < 0.0 || param > 1.0) {
      Rcpp::stop("kappa must be a finite scalar in [0, 1].");
    }
    return se_jacobian_impl(A, CfCriterion(param), T_init, normalize, oblique, eps);
  }
  if (method == "oblimin") {
    if (!is_valid_scalar_cpp(param)) {
      Rcpp::stop("gam must be a finite scalar.");
    }
    return se_jacobian_impl(A, ObliminCriterion(param), T_init, normalize, oblique, eps);
  }
  if (method == "geomin") {
    if (!is_valid_scalar_cpp(param) || param <= 0.0) {
      Rcpp::stop("delta must be a finite positive scalar.");
    }
    return se_jacobian_impl(A, GeominCriterion(param), T_init, normalize, oblique, eps);
  }
  if (method == "bentler") {
    return se_jacobian_impl(A, BentlerCriterion(), T_init, normalize, oblique, eps);
  }
  if (method == "bifactor") {
    if (general_col < 0 || static_cast<arma::uword>(general_col) >= k) {
      Rcpp::stop("general_col must be in [0, ncol(A) - 1].");
    }
    return se_jacobian_impl(A, BifactorCriterion(static_cast<arma::uword>(general_col)),
                            T_init, normalize, oblique, eps);
  }
  Rcpp::stop("Unknown rotation method for the standard-error Jacobian.");
}
