# LDW 26 Mar 2024: use option settings and store in cache environment

lavaan_cache_env <- new.env(parent = emptyenv())
# The variables stored in lavaan_cache_env environment all have a prefix
# to avoid conflicts. The prefixes used at the moment of writing
# (April 2026) are:
#                      for options: opt_
#    for trace debugging functions: trc_ and trace_
#     for the lavaan syntax parser: mdl_
# for tracking deprecated warnings: dpr_


# # function to get or set the switch to use c++ code in lavaanC
# lav_use_lavaanC <- uselavaanC <- function(x) {
#   if (missing(x)) {
#     if (!exists("opt.lavaanC", lavaan_cache_env)) {
#       assign("opt.lavaanC", requireNamespace("lavaanC", quietly = TRUE),
#               lavaan_cache_env)
#     }
#     return(get("opt.lavaanC", lavaan_cache_env))
#   } else {
#     if (!is.logical(x) || length(x) != 1L)
#       lav_msg_stop(gettext("'x' must be a scalar logical"))
#     if (x) {
#       if (!requireNamespace("lavaanC", quietly = TRUE)) {
#         lav_msg_warn(gettext("cannot use lavaanC, package not found."))
#         assign("opt.lavaanC", FALSE, lavaan_cache_env)
#         return(invisible(NULL))
#       }
#     }
#     assign("opt.lavaanC", x, lavaan_cache_env)
#     return(invisible(NULL))
#   }
# }

# functions to handle warn/debug/verbose options
#             (no longer in 'standard' options)
# if x not present returns the current value of opt_warn/debug/verbose
# if x present
#   if x different from current value, assign x to current value and return TRUE
#   else return FALSE
lav_warn <- function(x) {
  optwarn <- get0("opt_warn", lavaan_cache_env, ifnotfound = TRUE)
  if (missing(x)) {
    optwarn
  } else {
    setwarn <- as.logical(x)
    if (setwarn != optwarn) {
      if (setwarn) {
        # because default TRUE, removing value is the same as setting to TRUE
        rm("opt_warn", envir = lavaan_cache_env)
      } else {
        assign("opt_warn", FALSE, lavaan_cache_env)
      }
      TRUE
    } else {
      FALSE
    }
  }
}
lav_debug <- function(x) {
  optdebug <- get0("opt_debug", lavaan_cache_env, ifnotfound = FALSE)
  if (missing(x)) {
    optdebug
  } else {
    setdebug <- as.logical(x)
    if (setdebug != optdebug) {
      if (setdebug) {
        assign("opt_debug", TRUE, lavaan_cache_env)
      } else {
        # because default FALSE, removing value is the same as setting to FALSE
        rm("opt_debug", envir = lavaan_cache_env)
      }
      TRUE
    } else {
      FALSE
    }
  }
}
lav_verbose <- function(x) {
  optverbose <- get0("opt_verbose", lavaan_cache_env, ifnotfound = FALSE)
  if (missing(x)) {
    optverbose
  } else {
    setverbose <- as.logical(x)
    if (setverbose != optverbose) {
      if (setverbose) {
        assign("opt_verbose", TRUE, lavaan_cache_env)
      } else {
        # because default FALSE, removing value is the same as setting to FALSE
        rm("opt_verbose", envir = lavaan_cache_env)
      }
      TRUE
    } else {
      FALSE
    }
  }
}

# set the default options (including unspecified values "default")
lav_options_default <- function() {
  if (exists("opt_default", lavaan_cache_env)) {
    opt <- get("opt_default", lavaan_cache_env)
    return(opt)
  }
  # ---------------- preparation -----------------
  opt_default <- list()
  opt_check <- list()
  elm <- function(
    name = NULL,          # name of option, if length 2 first is sublist name
    dflt = NULL,          # default value
    chr = NULL,           # valid strings (names) and replacement values
    nm = NULL,            # valid numeric interval
    bl = FALSE,           # logical OK?
    oklen = c(1L, 1L),    # length > 1 OK, second negative to have a warning
    #                and not an error when length greater then abs(oklen[2])
    num2int = FALSE       # should numerical values be converted to int?
  ) {
    stopifnot(any(length(name) == 1:2))
    stopifnot(is.null(nm) || (length(nm) == 1 && grepl("^[][].*,.*[][]$", nm)))
    stopifnot(length(bl) == 1, is.logical(bl))
    stopifnot(is.null(chr) || is.character(chr))
    stopifnot(length(oklen) == 2, oklen[1] <= abs(oklen[2]))
    stopifnot(length(num2int) == 1, is.logical(num2int))

    # prepare list to store for checking option
    list2store <- list(oklen = oklen)
    if (!is.null(chr)) {
      if (is.null(names(chr))) names(chr) <- chr
      list2store$chr <- chr
    }
    if (!is.null(nm)) {
      first_in <- grepl("^\\[", nm)
      last_in <- grepl("\\]$", nm)
      elems <- as.numeric(strsplit(gsub("[][ ]", "", nm), ",")[[1]])
      if (num2int) {
        elems[elems == -Inf] <- -2e9
        elems[elems == Inf] <- 2e9
        elems <- as.integer(elems)
      }
      list2store$nm <- list(bounds = elems,
                            first.in = first_in, last.in = last_in)
    }
    if (bl) list2store$bl <- TRUE
    if (num2int) list2store$num2int <- TRUE

    # store default and list for checking
    if (length(name) == 1) name <- c("", name)
    if (name[1] != "") {
      if (is.null(opt_default[[name[1]]])) { # make sure sublists exist
        opt_default[[name[1]]] <<- list()
        sublist <- list()
        attr(sublist, "SUB") <- TRUE # indicate as sublist
        opt_check[[name[1]]] <<- sublist
      }
      opt_default[[name[1]]][[name[2]]] <<- dflt
      if (is.null(dflt)) opt_default[[name[1]]][name[2]] <<- list(NULL)
      opt_check[[name[1]]][[name[2]]] <<- list2store
    } else {
      opt_default[[name[2]]] <<- dflt
      if (is.null(dflt)) opt_default[name[2]] <<- list(NULL)
      opt_check[[name[2]]] <<- list2store
    }
    NULL
  }
  elmdup <- function(
    name = NULL, # name(s) of option
    from = NULL  # name(s) of option to duplicate
  ) {
    if (length(name) == 1) name <- c("", name)
    if (length(from) == 1) from <- c("", from)
    if (from[1] != "") {
      from_default <- opt_default[[from[1]]][[from[2]]]
      from_check <- opt_check[[from[1]]][[from[2]]]
    } else {
      from_default <- opt_default[[from[2]]]
      from_check <- opt_check[[from[2]]]
    }
    if (name[1] != "") {
      if (is.null(opt_default[[name[1]]])) { # make sure sublists exist
        opt_default[[name[1]]] <<- list()
        sublist <- list()
        attr(sublist, "SUB") <- TRUE # indicate as sublist
        opt_check[[name[1]]] <<- sublist
      }
      opt_default[[name[1]]][[name[2]]] <<- from_default
      opt_check[[name[1]]][[name[2]]] <<- from_check
    } else {
      opt_default[[name[2]]] <<- from_default
      opt_check[[name[2]]] <<- from_check
    }
  }
  # ------------------------- store options --------------------------
  elm("cmd", "lavaan", chr = c(lavaan = "lavaan", cfa = "cfa",
    growth = "growth", sem = "sem"))
  elm("model.type", "sem", chr = c(lavaan = "lavaan", cfa = "cfa",
            growth = "growth", sem = "sem", efa = "efa", path = "path",
            unrestricted = "unrestricted"))

  # global
  elm("mimic", "lavaan", chr = c(default = "lavaan", lavaan = "lavaan",
                                 regression = "lm", lisrel = "EQS",
                                 eqs = "EQS", lm = "lm", mplus = "Mplus"
  ))
  elm("gls.v11.mplus", "default", chr = "default", bl = TRUE)
  elm("gamma.vcov.mplus", "default", chr = "default", bl = TRUE)
  elm("gamma.wls.mplus", "default", chr = "default", bl = TRUE)
  elm("information.expected.mplus", "default", chr = "default", bl = TRUE)
  elm("rmsea.scaled.mplus", "default", chr = "default", bl = TRUE)

  # model modifiers
  elm("meanstructure", "default", chr = "default", bl = TRUE)
  elm("int.ov.free", FALSE, bl = TRUE)
  elm("int.lv.free", FALSE, bl = TRUE)
  elm("marker.int.zero", FALSE, bl = TRUE) # fix marker intercepts
  # free lv means
  elm("conditional.x", "default", chr = "default", bl = TRUE)
  elm("fixed.x", "default", chr = "default", bl = TRUE)
  elm("orthogonal", FALSE, bl = TRUE)
  elm("orthogonal.x", FALSE, bl = TRUE)
  elm("orthogonal.y", FALSE, bl = TRUE)
  elm("std.lv", FALSE, bl = TRUE)
  elm("correlation", FALSE, bl = TRUE) # correlation structure
  elm("effect.coding", FALSE, chr = c("",
                                      "loadings", "intercepts",
                                      "mg.lv.efa.variances", "mg.lv.variances",
                                      "mg.lv.means", "mg.lv.intercepts"),
      bl = TRUE, oklen = c(0L, 6L))
  elm("ceq.simple", FALSE, bl = TRUE) # treat simple eq cons special?
  elm("parameterization", "default", c(
    "default", "mml", "delta", "theta"))
  elm("auto.fix.first", FALSE, bl = TRUE)
  # if > 0 (and auto.fix.first = TRUE), switch to another marker if the first
  # indicator is a poor item; the value is the threshold below which a marker
  # indicator is considered 'poor' (in absolute value of its corrected
  # item-total correlation)
  elm("bad.marker.crit", 0.1, nm = "[0, 1]")
  elm("auto.fix.single", FALSE, bl = TRUE)
  elm("auto.var", FALSE, bl = TRUE)
  elm("auto.cov.lv.x", FALSE, bl = TRUE)
  # if TRUE, also add covariances between exogenous latent variables and
  # observed exogenous covariates (and imply auto.cov.lv.x = TRUE)
  elm("auto.cov.x", FALSE, bl = TRUE)
  elm("auto.cov.y", FALSE, bl = TRUE)
  elm("auto.th", FALSE, bl = TRUE)
  elm("auto.delta", FALSE, bl = TRUE)
  elm("auto.efa", FALSE, bl = TRUE)
  elm("composites", TRUE, bl = TRUE)
  # should the composite-indicator (co)variances (the 'T' matrix) be estimated
  # as free parameters, or fixed to their sample values?
  #   "fixed"   - fix T to the sample (co)variances (default for single-level)
  #   "free"    - estimate T as free parameters (needed for multilevel, where
  #               the same indicators may be composite indicators at >1 level)
  #   "default" - resolved later: "free" if multilevel, "fixed" otherwise
  elm("composites.cov", "default", chr = c("default", "fixed", "free"))

  # rotation
  elm("rotation", "geomin", chr = c(crawfer = "cf", crawford.ferguson = "cf",
    crawfordferguson = "cf", cf = "cf",
    varimax = "varimax", quartimax = "quartimax", orthomax = "orthomax",
    oblimin = "oblimin", quartimin = "quartimin", geomin = "geomin",
    entropy = "entropy", mccammon = "mccammon", infomax = "infomax",
    tandem1 = "tandem1", tandem2 = "tandem2", none = "none", promax = "promax",
    oblimax = "oblimax", bentler = "bentler", simplimax = "simplimax",
    target.strict = "target.strict", target = "pst", pst = "pst",
    cf.quartimax = "cf-quartimax", cf.varimax = "cf-varimax",
    cf.equamax = "cf-equamax", cf.parsimax = "cf-parsimax",
    cf.facparsim = "cf-facparsim", bi.quartimin = "biquartimin",
    biquartimin = "biquartimin", bi.geomin = "bigeomin", bigeomin = "bigeomin"
  ))
  elm("rotation.se", "bordered", chr = c("delta", "bordered"))

  # rotation-args sublist
  elm(c("rotation.args", "orthogonal"), FALSE, bl = TRUE)
  elm(c("rotation.args", "row_weights"), "default", chr = c(
    default = "default", kaiser = "kaiser", none = "none",
    cureton.mulaik = "cm", cm = "cm"))
  elm(c("rotation.args", "std_ov"), TRUE, bl = TRUE)
  elm(c("rotation.args", "geomin_epsilon"), 0.001, nm = "]0, 1.00]")
  # was 0.01 < 0.6-10
  elm(c("rotation.args", "orthomax_gamma"), 1, nm = "[0, 1]")
  elm(c("rotation.args", "cf_gamma"), 0, nm = "[0, 1]")
  elm(c("rotation.args", "oblimin_gamma"), 0, nm = "[0, 1000]")
  elm(c("rotation.args", "promax_kappa"), 4, nm = "[0, 1000]")
  elm(c("rotation.args", "target"), matrix(0, 0L, 0L), oklen = c(0, 1e+32))
  elm(c("rotation.args", "target_mask"), matrix(0, 0L, 0L), oklen = c(0, 1e+32))
  elm(c("rotation.args", "rstarts"), 30L, nm = "[0, 1e+07]")
  elm(c("rotation.args", "algorithm"), "gpa", chr = c("gpa", "pairwise"))
  elm(c("rotation.args", "reflect"), TRUE, bl = TRUE)
  elm(c("rotation.args", "order_lv_by"), "index",
      chr = c("sumofsquares", "index", "none"))
  elm(c("rotation.args", "gpa_tol"), 1e-05, nm = "]0, 0.01]")
  elm(c("rotation.args", "tol"), 1e-08, nm = "]0, 0.01]")
  elm(c("rotation.args", "warn"), FALSE, bl = TRUE)
  elm(c("rotation.args", "verbose"), FALSE, bl = TRUE)
  elm(c("rotation.args", "jac_init_rot"), TRUE, bl = TRUE)
  elm(c("rotation.args", "max_iter"), 10000L, nm = "[0, 1e+12]")
  elm(c("rotation.args", "mg_agreement"), FALSE, bl = TRUE)
  elm(c("rotation.args", "mg_agreement_weight"), 0.5, nm = "[0, 1]")
  elm(c("rotation.args", "mg_agreement_algorithm"), "pairwise",
    chr = c("pairwise", "lg"))
  elm(c("rotation.args", "mg_agreement_method"), "procrustes",
    chr = c("procrustes", "loading.alignment"))

  # full data
  elm("std.ov", FALSE, bl = TRUE)
  elm("missing", "default", chr = c(
    default = "default", ml = "ml", direct = "ml",
    ml.x = "ml.x", direct.x = "ml.x", fiml.x = "ml.x", fiml = "ml",
    two.stage = "two.stage", twostage = "two.stage", two.step = "two.stage",
    twostep = "two.stage", robust.two.stage = "robust.two.stage",
    robust.twostage = "robust.two.stage",
    robust.two.step = "robust.two.stage",
    robust.twostep = "robust.two.stage",
    two.stage.robust = "robust.two.stage",
    twostage.robust = "robust.two.stage",
    two.step.robust = "robust.two.stage",
    twostep.robust = "robust.two.stage",
    listwise = "listwise", pairwise = "pairwise",
    available.cases = "available.cases", doubly.robust = "doubly.robust"))
  elm("sampling.weights.normalization", "group", chr = c(
    "total", "group", "none"))
  elm("sampling.weights.type", "design", chr = c("design", "frequency"))
  elm("samplestats", TRUE, bl = TRUE)

  # summary data
  elm("sample.cov.rescale", "default", bl = TRUE)
  elm("sample.cov.robust", FALSE, bl = TRUE)
  elm("sample.icov", TRUE, bl = TRUE)
  elm("ridge", FALSE, bl = TRUE)
  elm("ridge.constant", "default", chr = "default", nm = "[0, Inf[")

  # multiple groups !!! group.label and group.partial capitals OK !!!
  elm("group.label", NULL, oklen = c(0L, 100L)) # no checks
  elm("group.equal", "", chr =
        c("", "none", "all", "loadings", "intercepts", "means",
          "composite.loadings", "composite.weights",
          "regressions", "residuals", "residual.covariances", "thresholds",
          "lv.variances", "lv.covariances"), oklen = c(0L, 100L))
  elm("group.partial", "", oklen = c(0L, 100L)) # no checks
  elm("group.w.free", FALSE, bl = TRUE)

  # clusters
  elm("level.label", NULL, oklen = c(0L, 100L)) # no checks

  # estimation
  elm("estimator", "default", chr = c(
    none = "none", default = "default", wlsmv = "wlsmv", ml = "ml", mlr = "mlr",
    mlf = "mlf", mlm = "mlm", mlmv = "mlmv", mlmvs = "mlmvs", gls = "gls",
    wls = "wls", wlsm = "wlsm", uls = "uls", ulsm = "ulsm", ulsmv = "ulsmv",
    pml = "pml", dls = "dls", ntrls = "ntrls", catml = "catml",
    dwls = "dwls", wlsmvs = "wlsmvs", ulsmvs = "ulsmvs", fml = "fml",
    umn = "fml", reml = "reml", mml = "mml", fabin = "fabin2",
    fabin2 = "fabin2", fabin3 = "fabin3", mgm = "mgm", guttman = "mgm",
    gutman = "mgm", gutmann = "mgm", guttman1952 = "mgm",
    js = "js", jsa = "jsa", james.stein = "js",
    james.stein.aggregated = "jsa", bentler = "bentler1982",
    bentler1982 = "bentler1982", miiv = "iv", iv = "iv",
    miiv.2sls = "iv", rbm = "rbm"
  ))
  elmdup("estimator.orig", "estimator")

  elm("estimator.args", list(), oklen = c(0L, 100L))
  elm("likelihood", "default", chr = c("default", "normal", "wishart"))
  elm("link", "default", chr = c("default", "logit", "probit"))
  elm("representation", "default", chr = c(
    default = "LISREL", lisrel = "LISREL", ram = "RAM"))
  elm("do.fit", TRUE, bl = TRUE)
  elm("bounds", "none", chr = c(
    "none", "default", "standard", "user", "wide", "wide.zerovar", "pos.var",
    "pos.ov.var", "pos.lv.var"))      # new in 0.6-6
  elm("rstarts", 0L, nm = "[0, 10000]", num2int = TRUE) # new in 0.6-18

  # inference
  elm("se", "default", chr = c(
    default = "default", none = "none", standard = "standard",
    robust.huber.white = "robust.huber.white", robust = "robust",
    robust.cluster = "robust.cluster",
    robust.cluster.sem = "robust.cluster.sem",
    sandwich = "robust.huber.white", robust.sem = "robust.sem",
    robust.sem.nt = "robust.sem.nt",
    two.stage = "two.stage", robust.two.stage = "robust.two.stage",
    bootstrap = "bootstrap", boot = "bootstrap", first.order = "first.order",
    robust.mlm.nt = "robust.sem.nt",
    robust.mlm = "robust.sem", robust.mlr = "robust.huber.white",
    observed = "observed", expected = "expected"),
    oklen = c(1L, -1L)
  )
  elm("test", "default", oklen = c(1L, 100L))
                  # checks for 'test' are in lav_test_rename !!!

  # delta method for defined parameter SEs (:=)
  # FALSE = first-order delta method (default)
  # TRUE  = second-order delta method
  elm("se.delta.second.order", FALSE, bl = TRUE)

  # SE method for defined parameters (:=)
  # "default" = delta method (respects se.delta.second.order)
  # "monte.carlo" = Monte Carlo method (Preacher & Selig 2012)
  elm("se.def", "default", chr = c(
    default = "default",
    delta = "default",
    monte.carlo = "monte.carlo",
    mc = "monte.carlo",
    MC = "monte.carlo"))

  # Monte Carlo settings (used when se.def = "monte.carlo")
  elm("monte.carlo", list(R = 20000L, seed = NULL), oklen = c(0L, 100L))

  # information (se + test)
  elm("information", c("default", "default"), chr = c(
    "default", "expected", "observed", "first.order"),
    oklen = c(1L, 2L))
  elm("h1.information", c("structured", "structured"), chr = c(
    "structured", "unstructured"), oklen =  c(1L, 2L))
  # multilevel h1 estimation with missing data (new in 0.7-1):
  # "em" (mimics Mplus) or "fiml" (cholesky + nlminb; old behavior)
  elm("h1.missing.method", "em", chr = c("em", "fiml"))
  elm("observed.information", c("hessian", "default"), chr = c(
    "default", "hessian", "h1"), oklen =  c(1L, 2L))

  # information se only
  elm("information.meat", "default",
      chr = c(default = "first.order", first.order = "first.order"))
  elm("h1.information.meat", "default", chr = c(
    "default", "structured", "unstructured"))

  # information for 'Omega' (yuan-benter test only)
  # ("first.order" is what omega.information becomes when information[2] is
  #  "first.order", eg estimator = "MLF"; it must be accepted here so that
  #  already-processed options can be fed back into lav_options_set(), as
  #  sam() does)
  elm("omega.information", "default", chr = c(
    "default", "expected", "observed", "first.order"
  ))
  elm("omega.h1.information", "default", chr = c(
    "default", "structured", "unstructured"
  ))
  elm("omega.information.meat", "default", chr = c(
    default = "first.order", first.order = "first.order"
  ))
  elm("omega.h1.information.meat", "default", chr = c(
    "default", "structured", "unstructured"
  ))

  # test statistic for fit measures
  elm("standard.test", "standard", oklen = c(1L, 100L))

  # test statistic for scaling
  elm("scaled.test", "standard", oklen = c(1L, 100L))

  # old approach trace.UGamma2
  elm("ug2.old.approach", FALSE, bl = TRUE)

  # bootstrap
  elm("bootstrap", list(R = 1000L))

  # gamma
  elm("gamma.n.minus.one", FALSE, bl = TRUE)
  elm("gamma.unbiased", FALSE, bl = TRUE)

  # optimization
  elm("control", list(), oklen = c(0L, 100L))
  elm("optim.method", "default", chr = c(
    "nlminb", "nlminb0", "nlminb1", "nlminb2",
    "bfgs", "l.bfgs.b", "gn", "default", "noniter", "none", "em"
  )) # gn for DLS, nlminb rest
  elm("optim.attempts", 4L, nm = "[1, 4]")
  elm("optim.force.converged", FALSE, bl = TRUE)
  # multilevel: fix parameters of saturated blocks at their h1 values
  # during optimization (new in 0.7-1)
  elm("optim.fix.saturated", TRUE, bl = TRUE)
  elm("optim.gradient", "analytic", chr = c(
    analytic = "analytic", analytical = "analytic",
    numeric = "numerical", numerical = "numerical"
  ))
  elm("optim.init_nelder_mead", FALSE, bl = TRUE)
  elm("optim.var.transform", "none", chr = c(
    "none", "sqrt"
  ))
  elm("optim.parscale", "none", chr = c(
    none = "none", st = "standardized", stand = "standardized",
    standardize = "standardized", standardized = "standardized"
  ))
  elm("optim.partrace", FALSE, bl = TRUE)
  elm("optim.dx.tol", 1e-03, nm = "]0, 0.01]") # not too strict
  elm("optim.bounds", list(), oklen = c(0L, 100L))
  # em-args sublist (EM algorithm for the h0 model; optim.method = "em")
  elm(c("em.args", "max_iter"), 10000L, nm = "[100, 1e8]", num2int = TRUE)
  elm(c("em.args", "fx_tol"), 1e-08, nm = "]0, 0.01]")
  elm(c("em.args", "dx_tol"), 1e-04, nm = "]0, 0.01]")
  elm(c("em.args", "zerovar_offset"), 0.0001, nm = "]0, 0.01]")
  # accelerate the EM iterations?
  elm(c("em.args", "acceleration"), "squarem",
      chr = c("squarem", "qn", "none"))
  # compute the loglikelihood as a byproduct of the E-step?
  # (two-level + missing data, plain iterations only)
  elm(c("em.args", "fused"), TRUE, bl = TRUE)
  # if the EM iterations end with a non-negligible gradient (a 'stall'),
  # refine the solution with nlminb, warm-started at the EM values?
  # (the EM solution is kept if nlminb fails to converge, or ends up
  # at a worse solution)
  elm(c("em.args", "nlminb_handoff"), TRUE, bl = TRUE)
  # if the nlminb hand-off is active, stop the EM phase early (using a
  # loosened fx_tol) and let nlminb finish? (a safety net reverts to
  # the strict fx_tol if nlminb fails)
  elm(c("em.args", "early_handoff"), TRUE, bl = TRUE)
  # if verbose = TRUE, also show the optimizer output of each (inner)
  # M-step? (off by default, so only the EM/SQUAREM cycles are shown)
  elm(c("em.args", "verbose"), FALSE, bl = TRUE)

  # em-h1-args sublist (EM algorithm for the unrestricted (h1) model)
  # "default" values are resolved later: single-level (missing data) uses
  # max_iter = 500L and tol = 1e-05 (tol checks the change in the parameter
  # values), multilevel uses max_iter = 5000L and tol = 1e-04 (tol checks
  # the change in the loglikelihood)
  elm(c("em.h1.args", "max_iter"), "default", chr = "default",
      nm = "[10, 1e7]", num2int = TRUE)
  elm(c("em.h1.args", "tol"), "default", chr = "default", nm = "]0, 0.01]")
  # minimum value a variance can take (multilevel only)
  elm(c("em.h1.args", "min_variance"), 1e-05, nm = "]0, 0.01]")
  elm(c("em.h1.args", "warn"), TRUE, bl = TRUE)
  # what to do if an EM estimated covariance matrix is (near) singular?
  elm(c("em.h1.args", "non_pd_action"), "warn", # "stop" breaks jmv
      chr = c("stop", "warn", "none"))
  elm(c("em.h1.args", "non_pd_tol"), 1e-05, nm = "]0, 0.01]")
  # accelerate the EM iterations?
  elm(c("em.h1.args", "acceleration"), "squarem",
      chr = c("squarem", "qn", "none"))
  # compute the loglikelihood as a byproduct of the E-step?
  # (two-level + missing data, plain iterations only)
  elm(c("em.h1.args", "fused"), TRUE, bl = TRUE)
  # gn-args sublist (Gauss-Newton; optim.method = "gn")
  # (< 0.7-1 these lived at the top level, as optim.gn.max.iter,
  #  optim.gn.tol.x and optim.gn.stephalf.max; the latter is gone: the
  #  Levenberg-Marquardt damping replaces the step-halving)
  elm(c("gn.args", "max_iter"), 200L, nm = "[100, 1e8]", num2int = TRUE)
  elm(c("gn.args", "tol_x"), 1e-05, nm = "]0, 0.01]")
  elm(c("gn.args", "tol_g"), 1e-06, nm = "]0, 0.01]")

  # numerical integration
  elm("integration.ngh", 21L, nm = "[1, 1000]", num2int = TRUE)

  # parallel
  elm("parallel", "no", chr = c(
    "no", "multicore", "snow"
  ))
  #maxcpu <- max(1L, parallel::detectCores() - 1L) # crashes if NA is returned
  maxcpu <- max(1L, parallel::detectCores() - 1L, na.rm = TRUE)
  # CRAN policy: never use more than 2 cores by default; users may request
  # more (up to maxcpu) explicitly.
  elm("ncpus", min(2L, maxcpu), nm = paste0("[1,", maxcpu, "]"))
  elm("cl", NULL, oklen = c(0L, 1L))
  elm("iseed", NULL, oklen = c(0L, 1L))

  # categorical
  elm("zero.add", c(0.5, 0.0), chr = "default",
      nm = "[0, 1]", oklen = c(1L, -2L))
  elm("zero.keep.margins", "default", chr = "default", bl = TRUE)
  elm("zero.cell.warn", FALSE, bl = TRUE) # since 0.6-1
  elm("allow.empty.cell", FALSE, bl = TRUE) # since 0.6-19
  elm("cat.wls.w", TRUE, bl = TRUE) # since 0.6-18

  # starting values (char values checked in lav_options_set())
  elm("start", "default", oklen = c(1L, 1000L))

  # sanity checks
  elm("check.start", TRUE, bl = TRUE)
  elm("check.sigma.pd", "chol", chr = c("chol", "eigen")) # stored in env
  elm("check.post", TRUE, bl = TRUE)
  elm("check.gradient", TRUE, bl = TRUE)
  elm("check.vcov", TRUE, bl = TRUE)
  elm("check.lv.names", TRUE, bl = TRUE)
  elm("check.lv.interaction", TRUE, bl = TRUE)
  elm("check.delta.cat.mediator", TRUE, bl = TRUE)

  # more models/info
  elm("h1", TRUE, bl = TRUE)
  elm("baseline", TRUE, bl = TRUE)
  elm("baseline.type", "independence", chr = c(indep = "independence",
    independence = "independence", nested = "nested"))
  elm("baseline.conditional.x.free.slopes", TRUE, bl = TRUE)
  elm("baseline.fixed.x.free.cov", TRUE, bl = TRUE)
  elm("fit.by.level", TRUE, bl = TRUE)
  elm("implied", TRUE, bl = TRUE)
  elm("loglik", TRUE, bl = TRUE)

  # storage of information
  elm("store.vcov", "default", chr = "default", bl = TRUE)

  # internal
  elm("parser", "open", chr = c(old = "old", orig = "old", new = "new",
          open = "open", c.r = "c.r", cr = "c.r", classic = "old"))

  # categorical
  elm("categorical", "default", chr = "default", bl = TRUE)

  # sort list and sublists
  for (nm in names(opt_default)) {
      if (is.list(opt_default[[nm]]))
        opt_default[[nm]] <- opt_default[[nm]][sort(names(opt_default[[nm]]))]
    }
  opt_default <- opt_default[sort(names(opt_default))]
  # ------------- store info in lavaan environment ---------------
  assign("opt_default", opt_default, lavaan_cache_env)
  assign("opt_check", opt_check, lavaan_cache_env)
  assign("opt_check_sigma_pd", "chol", lavaan_cache_env)

  # return defaults
  opt_default
}

# public function
lavOptions <- function(x = NULL, default = NULL, mimic = "lavaan") { # nolint
  lavoptions <- lav_options_default()

  # selection only
  if (!is.null(x)) {
    if (is.character(x)) {
      # lower case and dots only
      x <- lav_option_names(x)

      # check if x is in names(lavoptions)
      not_ok <- which(!x %in% names(lavoptions))
      if (length(not_ok) > 0L) {
        lav_msg_warn(gettextf(
          "option(s) %s not available", lav_msg_view(x[not_ok]))
        )
        x <- x[-not_ok]
      }

      # return requested option(s)
      if (length(x) == 0L) {
        default
      } else {
        lavoptions[x]
      }
    } else {
      lav_msg_stop(gettext("x= must be of type character"))
    }
  } else {
    lavoptions
  }
}
