# initial version YR 02/08/2010

# YR 28 Jan 2017: add lavOptions(), lav_options_default()
# LDW 16 Apr 2024: move lavOptions and lav_options_default to separate file

# help functions for lav_options_set ####
# help function to determine estimator 'group'
lav_options_estimatorgroup <- function(estimator) {
  goal <- switch(estimator,
    ml = ,
    mlf = ,
    mlm = ,
    mlmv = ,
    mlmvs = ,
    mlr = "ML",
    catml = "catML",
    dwls = ,
    wlsm = ,
    wlsmv = ,
    wlsmvs = "DWLS",
    uls = ,
    ulsm = ,
    ulsmv = ,
    ulsmvs = "ULS",
    none = "none",
    toupper(estimator)
  )
  goal
}
lav_options_checkinterval <- function(x, nm, num2int) {
  if (num2int) x <- as.integer(x)
  oks <- vapply(x, function(x1) {
    (x1 > nm$bounds[1] || (x1 == nm$bounds[1] && nm$first.in)) &&
      (x1 < nm$bounds[2] || (x1 == nm$bounds[2] && nm$last.in))
  }, TRUE)
  all(oks)
}
lav_options_checkvalues <- function(optname, optvalue, chr) {
  optvalid <- names(chr)
  if (is.null(optvalid)) optvalid <- chr
  if (any(optvalid == "empty.string")) {
    optvalid[optvalid == "empty.string"] <- ""
  }
  optvals <- gsub("[_-]", ".", tolower(optvalue))
  optvalsok <- match(optvals, optvalid)
  if (any(is.na(optvalsok))) {
    lav_msg_stop(sprintf(
      ngettext(
        length(optvalue[is.na(optvalsok)]),
        "invalid value in %1$s option: %2$s.",
        "invalid values in %1$s option: %2$s."
      ),
      optname,
      lav_msg_view(optvalue[is.na(optvalsok)], log_sep = "none")
    ))
  }
  as.vector(chr[optvalsok])
}
lav_options_check <- function(opts, opt_check, subname) {
  opt_names <- names(opts)
  hiddens <- startsWith(opt_names, ".")
  if (any(hiddens)) { # remove hidden options temporarily
    opts_hidden <- opts[hiddens]
    opts <- opts[!hiddens]
    opt_names <- opt_names[!hiddens]
  }
  check_names <- names(opt_check)
  match_opt <- match(opt_names, check_names)
  if (any(is.na(match_opt))) {
    lav_msg_stop(gettextf(
      "Some option(s) unknown: %s !",
      lav_msg_view(opt_names[is.na(match_opt)], log_sep = "none")
    ))
  }
  for (j in seq_along(opts)) {
    opt_name <- opt_names[j]
    opt_value <- opts[[j]]
    opt_check1 <- opt_check[[match_opt[j]]]
    if (!is.null(attr(opt_check1, "SUB"))) {
      opts[[j]] <- lav_options_check(
        opt_value, opt_check1,
        paste0(opt_name, "$")
      )
      next
    }
    # check length of option value
    if (length(opt_value) < opt_check1$oklen[1]) {
      lav_msg_stop(gettextf(
        "Length of option '%1$s' value must be at least %2$s.",
        paste0(subname, opt_name), opt_check1$oklen[1]
      ))
    }
    if (length(opt_value) > abs(opt_check1$oklen[2])) {
      if (opt_check1$oklen[2] > 0L) {
        lav_msg_stop(gettextf(
          "Length of option '%1$s' value must be maximum %2$s.",
          paste0(subname, opt_name), opt_check1$oklen[2]
        ))
      } else {
        lav_msg_warn(gettextf(
          "Length of option '%1$s' value should be maximum %2$s.
          Only first %3$s elements used.",
          paste0(subname, opt_name), -opt_check1$oklen[2],
          -opt_check1$oklen[2]
        ))
      }
    }
    if (is.null(opt_check1$bl)) opt_check1$bl <- FALSE
    if (!is.null(opt_check1$chr) || !is.null(opt_check1$nm) ||
      opt_check1$bl) {
      if (!opt_check1$bl || !is.logical(opt_value)) {
        if (!is.null(opt_check1$nm) && is.numeric(opt_value)) {
          num2int <- FALSE
          if (!is.null(opt_check1$num2int)) num2int <- opt_check1$num2int
          if (!lav_options_checkinterval(opt_value, opt_check1$nm, num2int)) {
            lav_msg_stop(gettextf(
              "Value(s) of option %1$s out of range (%2$s)!",
              paste0(subname, opt_name),
              paste0(
                opt_check1$nm$bounds[1],
                if (opt_check1$nm$first.in) " <= " else " < ",
                "x",
                if (opt_check1$nm$last.in) " <= " else " < ",
                opt_check1$nm$bounds[2]
              )
            ))
          }
        }
        if (!is.null(opt_check1$chr) && is.character(opt_value)) {
          opt_value <- lav_options_checkvalues(
            opt_name, opt_value,
            opt_check1$chr
          )
          opts[[j]] <- opt_value
        }
      }
    }
  }
  if (any(hiddens)) { # add hidden options
    opts <- modifyList(opts, opts_hidden)
  }
  opts
}
# this function collects and checks the user-provided options/arguments,
# and fills in the "default" values, or changes them in an attempt to
# produce a consistent set of values...
#
# returns a list with the named options
lav_options_set <- function(opt = NULL) {
  # check the presence of necessary hidden options ####
  if (is.null(opt$.categorical) || is.null(opt$.multilevel) ||
    is.null(opt$.clustered)) {
    lav_msg_fixme(
      ".categorical, .multilevel and .clustered must be present"
    )
  }

  # get opt_default and opt_check ####
  if (!exists("opt_check", lavaan_cache_env)) lav_options_default()
  opt_check <- get("opt_check", lavaan_cache_env)

  if (lav_debug()) {
    cat("lavaan DEBUG: lavaanOptions IN\n")
    str(opt)
    opt$optim.partrace <- TRUE
  }

  # options OPT for which there also exist OPT.args and opt$OPT is a list
  # -> split in opt$OPT and opt$OPT.args
  welke <- which(paste0(names(opt_check), ".args") %in% names(opt_check))
  for (j in welke) {
    optname <- names(opt_check)[j]
    optname_args <- paste0(optname, ".args")
    if (is.list(opt[[optname]])) {
      if (optname == "bootstrap" && !is.null(opt[[optname]][["R"]])) {
        # special case for bootstrap, instead of "bootstrap" replications
        # argument can be "R"
        opt[[optname]][[optname]] <- opt[[optname]][["R"]]
        opt[[optname]][["R"]] <- NULL
      }
      opt[[optname_args]] <- opt[[optname]]
      opt[[optname_args]][[optname]] <- NULL
      opt[[optname]] <- opt[[optname]][[optname]]
    }
  }

  if (is.list(opt$rotation.args)) {
    opt$rotation.args <- lav_snake_case(opt$rotation.args)
  }
  if (is.list(opt$em.args)) {
    opt$em.args <- lav_snake_case(opt$em.args)
  }
  if (is.list(opt$em.h1.args)) {
    opt$em.h1.args <- lav_snake_case(opt$em.h1.args)
  }

  # 'correlation' may be a character vector of (observed) variable names:
  # in that case, only those variables are standardized to unit variance
  # (a 'partial' correlation structure). We store the names in a hidden
  # option (.correlation.ov) and treat 'correlation' as a logical TRUE, so
  # the rest of the machinery (and all `if (correlation)` checks) work as
  # before. An empty .correlation.ov means 'all observed variables'.
  if (is.null(opt$.correlation.ov)) {
    opt$.correlation.ov <- character(0L)
  }
  if (is.character(opt$correlation)) {
    opt$.correlation.ov <- opt$correlation
    opt$correlation <- TRUE
  }

  # check options with definitions ####
  opt <- lav_options_check(opt, opt_check, "")

  # check option 'start'
  if (is.character(opt$start) && all(opt$start != c("default", "simple"))) {
    lav_msg_stop(gettext(
      "start option must be 'default', 'simple', a fitted object,
      a vector of parameter values, or a parameter table"
    ))
  }

  # first of all: set estimator ####
  if (opt$estimator == "default") {
    if (opt$.categorical) {
      opt$estimator <- "wlsmv"
    } else {
      opt$estimator <- "ml"
    }
  }

  # defaults for opt$sample.cov.rescale
  if (opt$sample.cov.rescale == "default") {
    opt$sample.cov.rescale <- switch(opt$estimator,
      dls = TRUE,
      fabin2 = ,
      fabin3 = ,
      mgm = ,
      js = ,
      jsa = ,
      bentler1982 = TRUE,
      iv = TRUE,
      "default"
    )
  }

  # option defaults specific for mimic=...
  opt <- lav_options_mimic(opt)

  # store opt$estimator as estimator.orig in upper case
  opt$estimator.orig <- toupper(opt$estimator)

  # rename names of test statistics if needed, check for invalid values ####
  opt$test <- lav_test_rename(opt$test, check = TRUE)

  # same for standard.test
  opt$standard.test <- lav_test_rename(opt$standard.test, check = TRUE)

  # same for scaled.test
  opt$scaled.test <- lav_test_rename(opt$scaled.test, check = TRUE)

  # rename names of se values, check illegal combinations se/estimator ####
  # pass-through function: may change value of information
  # for backwards compatibility (eg if se = "expected")
  opt <- lav_options_check_se(opt)

  # do.fit FALSE implies se="none" and test="none" (unless not default) ####
  if (!opt$do.fit) {
    if (opt$se == "default") opt$se <- "none"
    if (opt$test[1] == "default") opt$test <- "none"
  }

  # marker.int.fixed ####
  if (opt$marker.int.zero) {
    opt$meanstructure <- TRUE
    opt$int.ov.free <- TRUE
    if ((is.logical(opt$effect.coding) && opt$effect.coding) ||
      (is.character(opt$effect.coding) && nchar(opt$effect.coding) > 0L)) {
      lav_msg_stop(gettext(
        "effect coding cannot be combined with marker.int.zero = TRUE option"
      ))
    }
    if (opt$std.lv) {
      lav_msg_stop(gettext(
        "std.lv = TRUE cannot be combined with marker.int.zero = TRUE"
      ))
    }
  }

  # group.equal and group.partial ####
  if (length(opt$group.equal) > 0L && opt$group.equal[1] == "none") {
    opt$group.equal <- character(0)
  } else if (is.null(opt$group.equal) || all(nchar(opt$group.equal) == 0L)) {
    opt$group.equal <- character(0)
  } else if ("all" %in% opt$group.equal) {
    # "all": constrain all (free) parameters to be equal across groups
    # (expand to the full set of parameter types; any other elements that
    #  were given together with "all" become redundant)
    opt$group.equal <- c("loadings", "composite.weights", "intercepts",
                         "means", "thresholds", "regressions", "residuals",
                         "residual.covariances", "lv.variances",
                         "lv.covariances")
  }

  if (is.null(opt$group.partial) || all(nchar(opt$group.partial) == 0L)) {
    opt$group.partial <- character(0)
  } else if (length(opt$group.partial) == 0) {
    # nothing to do
  } else {
    # strip white space
    opt$group.partial <- gsub("[[:space:]]+", "", opt$group.partial)
  }

  # if categorical, and group.equal contains "intercepts", also add
  # thresholds (and vice versa)
  # not any longer since 0.6-20
  # if (opt$.categorical && any("intercepts" == opt$group.equal)) {
  #  opt$group.equal <- unique(c(opt$group.equal, "thresholds"))
  # }
  # if (opt$.categorical && any("thresholds" == opt$group.equal)) {
  #  opt$group.equal <- unique(c(opt$group.equal, "intercepts"))
  # }

  # clustered ####
  # brute-force override (for now)
  if (opt$.clustered && !opt$.multilevel) {
    opt$meanstructure <- TRUE

    # single-level clustered continuous ML: the pairwise / available.cases
    # missing-data methods are not supported (they were silently fed into the
    # cluster-robust machinery, producing nonsense estimates and a failing
    # baseline model, or crashing deep inside with a cryptic "NA/NaN/Inf in
    # foreign function call" error); the categorical (D)WLS + cluster path
    # handles these via muthen1984() and is left untouched
    if (!opt$.categorical &&
        any(opt$missing == c("pairwise", "available.cases"))) {
      lav_msg_stop(gettextf(
        "missing = %1$s is not supported for clustered (cluster =) continuous
         models; use missing = %2$s or missing = %3$s instead.",
        dQuote(opt$missing), dQuote("ml"), dQuote("listwise")))
    }

    if (opt$estimator == "mlr") {
      opt$estimator <- "ml"
      opt$test <- "yuan.bentler.mplus"
      opt$se <- "robust.cluster"
    } else if (opt$estimator == "mlm") {
      opt$estimator <- "ml"
      opt$test <- "satorra.bentler"
      opt$se <- "robust.cluster.sem"
    } else if (opt$.categorical && opt$estimator != "pml") {
      opt$test <- "satorra.bentler"
      opt$se <- "robust.cluster.sem"
    }

    # test ####
    if (length(opt$test) == 1L && opt$test == "default") {
      opt$test <- "yuan.bentler.mplus"
    } else if (all(opt$test %in% c(
      "none", "standard",
      "satorra.bentler",
      "yuan.bentler", "yuan.bentler.mplus"
    ))) {
      # nothing to do
    } else if (opt$se == "robust") {
      opt$test <- "yuan.bentler.mplus"
    } else {
      lav_msg_stop(
        gettextf(
          "`test' argument must be one of %s in the clustered case",
          lav_msg_view(c(
            "none", "yuan.bentler", "yuan.bentler.mplus",
            "satorra.bentler"
          ), log_sep = "or")
        )
      )
    }

    # se ####
    if (opt$se == "default") {
      opt$se <- "robust.cluster"
    } else if (any(opt$se == c(
      "none", "robust.cluster",
      "robust.cluster.sem"
    ))) {
      # nothing to do
    } else if (opt$se == "robust") {
      opt$se <- "robust.cluster"
    }

    # information ####
    if (opt$information[1] == "default") {
      if (opt$se == "robust.cluster" && opt$estimator == "ml") {
        opt$information[1] <- "observed"
      } else {
        opt$information[1] <- "expected"
      }
    }
    if (length(opt$information) > 1L && opt$information[2] == "default") {
      if (opt$se == "robust.cluster") {
        opt$information[2] <- "observed"
      } else {
        opt$information[2] <- "expected"
      }
    }
  }

  # cluster-robust SEs were explicitly requested, but no cluster= variable
  # was given: the requested estimator is undefined without cluster data
  # (before, lavaan silently computed a non-cluster-robust sandwich that did
  # not match any documented estimator)
  if (!opt$.clustered &&
      any(opt$se == c("robust.cluster", "robust.cluster.sem"))) {
    lav_msg_stop(gettextf(
      "se = %s requires clustered data; please provide the cluster= argument.",
      dQuote(opt$se)))
  }

  # composites ####
  # resolve composites.cov = "default": the composite-indicator (co)variances
  # (the 'T' matrix) are fixed to their sample values for single-level models,
  # but estimated freely for multilevel models (where the same indicators may
  # serve as composite indicators at more than one level, and fixing T at every
  # level would over-constrain the model).
  if (identical(opt$composites.cov, "default")) {
    if (opt$.multilevel) {
      opt$composites.cov <- "free"
    } else {
      opt$composites.cov <- "fixed"
    }
  }

  # multilevel ####
  # brute-force override (for now)
  if (opt$.multilevel) {
    opt$meanstructure <- TRUE

    # no estimator supports sampling weights in the two-level case (yet);
    # before 0.7-1 the ML machinery silently ignored them
    if (!is.null(opt$.sampling.weights) && opt$.sampling.weights) {
      lav_msg_stop(gettext(
        "sampling weights are not supported for two-level models (yet)."))
    }

    # only listwise deletion and full-information ML can handle missing
    # values in the two-level case; the other methods (two.stage,
    # robust.two.stage, pairwise, available.cases, doubly.robust) are
    # single-level methods (before 0.7-1 these crashed deep inside the
    # saturated-model EM with a cryptic error)
    if (any(opt$missing == c(
      "two.stage", "robust.two.stage", "pairwise",
      "available.cases", "doubly.robust"
    ))) {
      lav_msg_stop(gettextf(
        "missing = %1$s is not supported for two-level models; use
         missing = %2$s or missing = %3$s instead.",
        dQuote(opt$missing), dQuote("ml"), dQuote("listwise")))
    }

    # two-level + conditional.x: the expected information would need the
    # Delta matrix in the unconditional statistic space (the parameter-space
    # kernel lav_mvn_cl_info_expected_delta works with unconditional
    # moments); until that conversion exists, only observed/first.order
    # information is supported (the default)
    if (isTRUE(opt$conditional.x) && any(opt$information == "expected")) {
      lav_msg_stop(gettext(
        "information = \"expected\" is not supported (yet) for two-level
         models with conditional.x = TRUE; use \"observed\" (the default)
         or \"first.order\"."))
    }

    # ditto for a user-requested unstructured h1: the unstructured
    # (unconditional) h1 information cannot be combined with the
    # conditional.x Delta (before 0.7-1 this crashed with a cryptic
    # "non-conformable arguments" error); note that the internal
    # yuan.bentler.mplus route (which never mixes the two spaces) does
    # not pass through this check
    if (isTRUE(opt$conditional.x) &&
        any(opt$h1.information == "unstructured")) {
      lav_msg_stop(gettext(
        "h1.information = \"unstructured\" is not supported (yet) for
         two-level models with conditional.x = TRUE."))
    }

    # two-level least-squares estimation? (WLS/WLSM(V)/ULS(M,MV)/DWLS)
    multilevel_wls <-
      lav_options_estimatorgroup(opt$estimator) %in% c("WLS", "DWLS", "ULS")

    if (multilevel_wls) {
      # two-level (D)WLS: continuous, complete data (for now)
      if (!opt$missing %in% c("default", "listwise")) {
        lav_msg_stop(gettextf(
          "missing = %s is not supported for two-level (D)WLS estimation;
          only complete data (missing = \"listwise\") is supported (for now).",
          dQuote(opt$missing)))
      }
      if (isTRUE(opt$conditional.x) && !opt$.categorical) {
        lav_msg_stop(gettext(
          "conditional.x = TRUE is not supported for two-level (D)WLS
          estimation with continuous-only data (yet)."))
      }
      if (opt$.categorical) {
        # categorical two-level (D)WLS:
        # - the stage-wise estimation works in the theta parameterization
        #   (the within-level latent response variances are fixed to 1)
        if (opt$parameterization == "default") {
          opt$parameterization <- "theta"
        } else if (opt$parameterization != "theta") {
          lav_msg_stop(gettext(
            "two-level (D)WLS estimation with categorical data requires
            parameterization = \"theta\"."))
        }
        # - no (continuous) h1 model; the unrestricted statistics come
        #   from the stage-wise (univariate + bivariate) estimation
        opt$h1 <- FALSE
      } else {
        # the sample 'statistics' are the h1 (saturated) estimates
        opt$h1 <- TRUE
      }

      # test
      if (length(opt$test) == 1L && opt$test == "default") {
        # ok, will be set later
      } else if (all(opt$test %in% c(
        "none", "standard", "satorra.bentler",
        "scaled.shifted", "mean.var.adjusted"
      ))) {
        # nothing to do
      } else {
        lav_msg_stop(gettextf(
          "`test' argument must be one of %s in the two-level (D)WLS case",
          lav_msg_view(c(
            "none", "standard", "satorra.bentler",
            "scaled.shifted", "mean.var.adjusted"
          ), log_sep = "or")
        ))
      }

      # se
      if (opt$se == "default") {
        # ok, will be set later
      } else if (any(opt$se == c(
        "none", "standard", "robust.sem", "bootstrap"
      ))) {
        # nothing to do (bootstrap uses the cluster bootstrap)
      } else if (opt$se == "robust") {
        opt$se <- "robust.sem"
      } else {
        lav_msg_stop(gettextf(
          "`se' argument must be one of %s in the two-level (D)WLS case",
          lav_msg_view(c("none", "standard", "robust.sem", "bootstrap"),
            log_sep = "or"
          )
        ))
      }

      # information
      if (opt$information[1] == "default") {
        opt$information[1] <- "expected"
      }
      if (length(opt$information) > 1L && opt$information[2] == "default") {
        opt$information[2] <- "expected"
      }
    } else {
      # test
      if (length(opt$test) == 1L && opt$test == "default") {
        # ok, will be set later
      } else if (all(opt$test %in% c(
        "none", "standard", "yuan.bentler",
        "yuan.bentler.mplus"
      ))) {
        # nothing to do
      } else {
        lav_msg_stop(gettextf(
          "`test' argument must be one of %s in the multilevel case",
          lav_msg_view(c(
            "none", "standard", "yuan.bentler",
            "yuan.bentler.mplus"
          ), log_sep = "or")
        ))
      }

      # se
      if (opt$se == "default") {
        # ok, will be set later
      } else if (any(opt$se == c(
        "none", "standard", "robust.huber.white", "sandwich", "bootstrap"
      ))) {
        # nothing to do (bootstrap uses the cluster bootstrap; see lavBootstrap)
      } else if (opt$se == "robust") {
        opt$se <- "robust.huber.white"
      } else {
        lav_msg_stop(gettextf(
          "`se' argument must be one of %s in the multilevel case",
          lav_msg_view(c("none", "standard", "robust.huber.white", "bootstrap"),
            log_sep = "or"
          )
        ))
      }

      # information
      if (opt$information[1] == "default") {
        opt$information[1] <- "observed"
      }
      if (length(opt$information) > 1L && opt$information[2] == "default") {
        opt$information[2] <- "observed"
      }
    }
  }

  # random slopes (rv() modifier) ####
  if (is.null(opt$.rv)) {
    opt$.rv <- FALSE
  }
  if (opt$.rv) {
    # random slopes are only available for two-level models with
    # continuous data, complete data, estimator ML (for now)
    if (!opt$.multilevel) {
      lav_msg_stop(gettext(
        "random slopes (rv() modifier) are only supported for two-level
         models (with a cluster= argument)."))
    }
    if (opt$.categorical) {
      lav_msg_stop(gettext(
        "random slopes (rv() modifier) are not supported for categorical
         data."))
    }
    if (!is.null(opt$.sampling.weights) && opt$.sampling.weights) {
      lav_msg_stop(gettext(
        "random slopes (rv() modifier) are not supported in combination
         with sampling weights."))
    }
    if (!opt$estimator %in% c("ml", "mlr", "default")) {
      lav_msg_stop(gettextf(
        "random slopes (rv() modifier) require estimator = %s or %s.",
        dQuote("ML"), dQuote("MLR")))
    }
    if (opt$missing %in% c("ml.x", "two.stage", "robust.two.stage")) {
      lav_msg_stop(gettextf(
        "random slopes (rv() modifier) support missing = %s or
         missing = %s only.", dQuote("listwise"), dQuote("ml")))
    }
    if (isTRUE(opt$conditional.x)) {
      lav_msg_stop(gettext(
        "random slopes (rv() modifier) are not supported in combination
         with conditional.x = TRUE."))
    }
    # note: fixed.x = FALSE may simply mean that the model contains no
    # observed exogenous covariates at all (e.g., all random slopes
    # involve latent covariates); if an *observed* covariate does carry
    # a random slope while fixed.x = FALSE, lav_mvn_cl_rs_info() stops
    # with a specific message (the covariate must be exogenous/fixed.x)
    # no saturated (h1) model exists for random-slope models: no
    # chi-square test statistic, no baseline model, no (incremental)
    # fit indices; only logl/AIC/BIC are available
    # note: we do keep h1 = TRUE: the (standard, no-random-slope)
    # saturated two-level moments provide good starting values; but the
    # h1 loglikelihood is NOT comparable to the (conditional-on-x)
    # loglikelihood of the random-slope model
    opt$baseline <- FALSE
    opt$fit.by.level <- FALSE
    # note: estimator = "MLR" implies test = "yuan.bentler.mplus" (set
    # in lav_options_mimic); this is not an explicit user request, so
    # we do not warn about it
    if (!all(opt$test %in% c("default", "none")) &&
        !(opt$estimator == "mlr" &&
          all(opt$test %in% c("yuan.bentler", "yuan.bentler.mplus")))) {
      lav_msg_warn(gettext(
        "test statistics are not available (yet) for models with random
         slopes; test set to \"none\"."))
    }
    opt$test <- "none"
    if (opt$se == "robust") {
      opt$se <- "robust.huber.white"
    }
    if (!opt$se %in% c("default", "none", "standard",
                       "robust.huber.white")) {
      lav_msg_stop(gettextf(
        "`se' argument must be one of %s for models with random slopes.",
        lav_msg_view(c("none", "standard", "robust.huber.white"),
                     log_sep = "or")))
    }
    # standard errors: numerical hessian of the observed-data loglikelihood
    opt$observed.information[1] <- "hessian"
    if (length(opt$observed.information) > 1L) {
      opt$observed.information[2] <- "hessian"
    }
    # the gradient is analytic (adjoint pass; numerical fallback);
    # optim.gradient = "numerical" remains available as an escape hatch
    # optimizer: nlminb (default) or em
    if (opt$optim.method == "gn") {
      lav_msg_warn(gettext(
        "optim.method = \"gn\" is not available for models with random
         slopes; switching to nlminb."))
      opt$optim.method <- "nlminb"
    }
  }

  # em.h1.args: resolve "default" values
  # note: the single-level (missing data) EM converges when the change in
  # the parameter values is smaller than tol (tol was 1e-06 < 0.6-9), while
  # the multilevel EM converges when the change in the loglikelihood is
  # smaller than tol
  if (identical(opt$em.h1.args$max_iter, "default")) {
    if (opt$.multilevel) {
      opt$em.h1.args$max_iter <- 5000L
    } else {
      opt$em.h1.args$max_iter <- 500L
    }
  }
  if (identical(opt$em.h1.args$tol, "default")) {
    if (opt$.multilevel) {
      opt$em.h1.args$tol <- 1e-04
    } else {
      opt$em.h1.args$tol <- 1e-05
    }
  }

  # missing ####
  if (opt$missing == "default") {
    opt$missing <- "listwise"
  } else if (opt$missing == "ml") {
    if (opt$.categorical) {
      lav_msg_stop(gettextf(
        "missing = %s not available in the categorical setting",
        dQuote(opt$missing)
      ))
    }
    # for the (continuous) least-squares estimators, ML is not available;
    # interpret missing = "ml"/"fiml" as a generic request to handle missing
    # values, and silently switch to two-stage estimation
    if (any(opt$estimator == c("uls", "gls", "wls", "dls"))) {
      opt$missing <- "two.stage"
    } else if (any(opt$estimator == c(
      "mlm", "mlmv", "wlsm", "wlsmv", "ulsm", "ulsmv", "pml"
    ))) {
      lav_msg_stop(gettextf(
        "missing=%1$s is not allowed for estimator %2$s",
        dQuote(opt$missing),
        dQuote(lav_options_estimatorgroup(opt$estimator.orig))
      ))
    }
  } else if (opt$missing == "ml.x") {
    if (opt$.categorical) {
      lav_msg_stop(gettextf(
        "missing = %s not available in the categorical setting",
        dQuote(opt$missing)
      ))
    }
    # see missing = "ml" above: handle missing values via two-stage
    if (any(opt$estimator == c("uls", "gls", "wls", "dls"))) {
      opt$missing <- "two.stage"
    } else if (any(opt$estimator == c(
      "mlm", "mlmv", "wlsm", "wlsmv", "ulsm", "ulsmv", "pml"
    ))) {
      lav_msg_stop(gettextf(
        "missing=%1$s is not allowed for estimator %2$s",
        dQuote(opt$missing),
        dQuote(lav_options_estimatorgroup(opt$estimator.orig))
      ))
    }
  } else if (opt$missing == "two.stage") {
    if (opt$.categorical) {
      lav_msg_stop(gettextf(
        "missing = %s not available in the categorical setting",
        dQuote(opt$missing)
      ))
    }
    # uls/gls/wls/dls are allowed (continuous data only): they use the
    # two-stage EM moments for estimation and the robust.sem sandwich for SEs
    if (any(opt$estimator == c(
      "mlm", "mlmv", "wlsm", "wlsmv",
      "ulsm", "ulsmv", "pml", "mml"
    ))) {
      lav_msg_stop(gettextf(
        "missing=%1$s is not allowed for estimator %2$s",
        dQuote(opt$missing),
        dQuote(lav_options_estimatorgroup(opt$estimator.orig))
      ))
    }
  } else if (opt$missing == "robust.two.stage") {
    if (opt$.categorical) {
      lav_msg_stop(gettextf(
        "missing = %s not available in the categorical setting",
        dQuote(opt$missing)
      ))
    }
    if (any(opt$estimator == c(
      "mlm", "mlmv", "wlsm", "wlsmv",
      "ulsm", "ulsmv", "pml", "mml"
    ))) {
      lav_msg_stop(gettextf(
        "missing=%1$s is not allowed for estimator %2$s",
        dQuote(opt$missing),
        dQuote(lav_options_estimatorgroup(opt$estimator.orig))
      ))
    }
  } else if (opt$missing == "doubly.robust") {
    if (opt$estimator != "pml") {
      lav_msg_stop(gettextf(
        "missing=%s option only available for estimator PML",
        dQuote(opt$missing)
      ))
    }
  }

  # check missing ####
  if (any(opt$missing == c("ml", "ml.x")) &&
    opt$se %in% c("robust.sem", "robust.sem.nt")) {
    lav_msg_warn(gettextf(
      "missing will be set to %1$s for se = %2$s.",
      dQuote("listwise"), dQuote(opt$se)
    ))
    opt$missing <- "listwise"
  }
  if (any(opt$missing == c("ml", "ml.x")) &&
    any(opt$test %in% c(
      "satorra.bentler",
      "mean.var.adjusted", "scaled.shifted",
      "mean.var.adjusted.corrected", "scaled.shifted.corrected"
    ))) {
    lav_msg_warn(gettextf(
      "missing will be set to %s for satorra.bentler style test",
      dQuote("listwise")
    ))
    opt$missing <- "listwise"
  }

  # checks if missing = "two.stage" or "robust.two.stage" ####
  if (any(opt$missing == c("two.stage", "robust.two.stage"))) {
    opt$meanstructure <- TRUE
    # the (continuous) least-squares estimators do not have a dedicated
    # two-stage SE machinery; instead, they reuse the standard robust.sem
    # sandwich, with the two-stage NACOV of the EM moments (computed in
    # lav_samplestats); only the (point) estimation uses the EM moments
    ls_two_stage <- any(opt$estimator == c("uls", "gls", "wls", "dls"))
    # se
    if (ls_two_stage) {
      if (opt$se == "default") {
        opt$se <- "robust.sem"
      } else if (!any(opt$se == c("none", "robust.sem", "robust.sem.nt"))) {
        lav_msg_warn(gettextf(
          "se will be set to %1$s if missing = %2$s and estimator = %3$s",
          dQuote("robust.sem"), dQuote(opt$missing),
          dQuote(lav_options_estimatorgroup(opt$estimator.orig))
        ))
        opt$se <- "robust.sem"
      }
    } else if (opt$se == "default") {
      if (opt$missing == "two.stage") {
        opt$se <- "two.stage"
      } else {
        opt$se <- "robust.two.stage"
      }
    } else if (opt$missing == "two.stage" &&
      opt$se == "two.stage") {
      # nothing to do
    } else if (opt$missing == "robust.two.stage" &&
      opt$se == "robust.two.stage") {
      # nothing to do
    } else {
      lav_msg_warn(gettextf(
        "se will be set to %1$s if missing = %2$s",
        dQuote(opt$missing), dQuote(opt$missing)
      ))
      opt$se <- opt$missing
    }

    # the information/observed.information overrides below are specific to the
    # ML two-stage SE machinery; the least-squares + robust.sem path keeps the
    # estimator's own (default) information settings
    if (!ls_two_stage) {
      # information
      if (opt$information[1] == "default") {
        # for both two.stage and robust.two.stage
        opt$information[1] <- "observed"
      } else if (opt$information[1] == "first.order") {
        lav_msg_warn(gettextf(
          "information will be set to %1$s if missing = %2$s",
          dQuote("observed"), dQuote(opt$missing)
        ))
        opt$information[1] <- "observed"
      }

      # observed.information (ALWAYS "h1" for now)
      opt$observed.information[1] <- "h1"
      opt$observed.information[2] <- "h1"

      # new in 0.6-9: ALWAYS h1.information = "unstructured"
      opt$h1.information <- c("unstructured", "unstructured")

      if (length(opt$information) > 1L && opt$information[2] == "default") {
        # for both two.stage and robust.two.stage
        opt$information[2] <- "observed"
      }
    }

    # test
    if (length(opt$test) > 1L) {
      lav_msg_warn(gettextf(
        "test= argument can only contain a single element if missing = %s
        (taking the first)", dQuote(opt$missing)
      ))
      opt$test <- opt$test[1]
    }

    if (length(opt$test) == 1L && opt$test == "default") {
      opt$test <- "satorra.bentler"
    } else if (length(opt$test) == 1L && any(
      opt$test == c("satorra", "sb", "satorra.bentler", "satorra-bentler")
    )) {
      opt$test <- "satorra.bentler"
    } else {
      lav_msg_warn(gettextf(
        "test will be set to %1$s if missing = %2$s",
        dQuote("satorra.bentler"), dQuote(opt$missing)
      ))
      opt$test <- "satorra.bentler"
    }
  }

  # meanstructure ####
  if (is.logical(opt$meanstructure)) {
    if (opt$meanstructure == FALSE) {
      if (any(opt$missing == c("ml", "ml.x", "two.stage"))) {
        lav_msg_warn(gettextf(
          "missing argument %s forces meanstructure = TRUE",
          opt$missing
        ))
      }
    }
  } else if (opt$meanstructure == "default") {
    # by default: no meanstructure!
    if (opt$estimator == "pml") {
      opt$meanstructure <- TRUE
    } else {
      opt$meanstructure <- FALSE
    }
  }

  # force meanstructure = TRUE if either int.lv.free or int.ov.free is TRUE
  # but only if lavaan() was used
  if (opt$cmd == "lavaan" && (opt$int.lv.free || opt$int.ov.free)) {
    opt$meanstructure <- TRUE
  }

  # bootstrap ####
  if (is.numeric(opt$bootstrap)) {
    opt$bootstrap <- list(R = as.integer(opt$bootstrap))
  }
  if (opt$se == "bootstrap") {
    opt$information[1] <- "observed"
    if (length(opt$information) > 1L && opt$information[2] == "default") {
      opt$information[2] <- "observed"
    }
  }

  # specific per estimator (group) ####
  opt <- switch(opt$estimator,
    ml = ,
    mlf = ,
    mlm = ,
    mlmv = ,
    mlmvs = ,
    mlr = lav_options_est_ml(opt),
    gls = lav_options_est_gls(opt),
    ntrls = lav_options_est_ntrls(opt),
    catml = lav_options_est_catml(opt),
    wls = lav_options_est_wls(opt),
    dls = lav_options_est_dls(opt),
    dwls = ,
    wlsm = ,
    wlsmv = ,
    wlsmvs = lav_options_est_dwls(opt),
    uls = ,
    ulsm = ,
    ulsmv = ,
    ulsmvs = lav_options_est_uls(opt),
    pml = lav_options_est_pml(opt),
    fml = lav_options_est_fml(opt),
    reml = lav_options_est_reml(opt),
    mml = lav_options_est_mml(opt),
    fabin2 = ,
    fabin3 = ,
    mgm = ,
    js = ,
    jsa = ,
    bentler1982 = lav_options_est_fabin(opt),
    iv = lav_options_est_iv(opt),
    rbm = lav_options_est_rbm(opt),
    lav_options_est_none(opt) # estimator = none
  )

  # after code specific to estimator types                          ####
  # optim.method - if still "default" at this point -> set to "nlminb";
  # new in 0.7-1: for two-level models with missing = "ml" (and an
  # ML-family estimator), the EM algorithm is the default optimizer
  # (more robust; this mimics Mplus); if the EM optimizer turns out not
  # to support the model (e.g., multiple groups, conditional.x), we
  # quietly fall back to "nlminb" later (see lav_lavaan_step11_optim)
  if (opt$optim.method == "default") {
    if (opt$.multilevel && opt$missing %in% c("ml", "ml.x") &&
        lav_options_estimatorgroup(opt$estimator) == "ML") {
      opt$optim.method <- "em"
      opt$.optim.em.fallback <- TRUE
    } else {
      opt$optim.method <- "nlminb"
    }
  }

  # special stuff for categorical
  if (opt$.categorical) {
    opt$meanstructure <- TRUE # Mplus style
    if (lav_options_estimatorgroup(opt$estimator) == "ML") {
      lav_msg_stop(gettext(
        "estimator ML for ordered data is not supported yet; use WLSMV instead."
      ))
    }
  }

  # link
  if (opt$link == "logit") {
    if (opt$estimator != "mml") {
      lav_msg_warn(gettextf(
        "link will be set to %1$s for estimator = %2$s",
        dQuote("probit"), dQuote(opt$estimator)
      ))
    }
  }

  # likelihood approach (wishart or normal) + sample.cov.rescale
  if (!any(lav_options_estimatorgroup(opt$estimator) ==
    c("ML", "REML", "PML", "FML", "NTRLS", "catML"))) {
    # if(opt$likelihood != "default") {
    #    lav_msg_stop(gettext(
    #    "likelihood argument is only relevant if estimator = ML"))
    # }
    if (opt$sample.cov.rescale == "default") {
      opt$sample.cov.rescale <- FALSE
    } # else {
    #    lav_msg_warn(gettext(
    #    "sample.cov.rescale argument is only relevant if estimator = ML"))
    # }
  } else { # ml and friends
    if (any(lav_options_estimatorgroup(opt$estimator) == c("PML", "FML"))) {
      opt$likelihood <- "normal"
    } else if (opt$likelihood == "default") {
      opt$likelihood <- "normal"
    }

    if (opt$sample.cov.rescale == "default") {
      opt$sample.cov.rescale <- FALSE
      if (opt$likelihood == "normal") {
        opt$sample.cov.rescale <- TRUE
      }
    }
  }

  # se information
  if (opt$information[1] == "default") {
    if (any(opt$missing == c("ml", "ml.x")) ||
      any(opt$se == c("robust.huber.white", "first.order"))) {
      # nchar(opt$constraints) > 0L) {
      opt$information[1] <- "observed"
    } else {
      opt$information[1] <- "expected"
    }
  }

  # first.order information can not be used with robust
  if (opt$information[1] == "first.order" &&
    any(opt$se == c("robust.huber.white", "robust.sem", "robust.sem.nt"))) {
    lav_msg_stop(gettextf(
      "information must be either %s if robust standard errors are requested.",
      lav_msg_view(c("expected", "observed"), log_sep = "or")
    ))
  }

  # test information
  if (length(opt$information) == 1L) {
    opt$information <- rep(opt$information, 2L)
  }
  if (opt$information[2] == "default") {
    if (any(opt$missing == c("ml", "ml.x")) ||
      any(opt$se == c("robust.huber.white", "first.order"))) {
      # nchar(opt$constraints) > 0L) {
      opt$information[2] <- "observed"
    } else {
      opt$information[2] <- "expected"
    }
  }

  # first.order information cannot be used with robust
  if (opt$information[2] == "first.order" &&
    any(opt$test %in% c(
      "satorra.bentler", "yuan.bentler",
      "yuan.bentler.mplus",
      "mean.var.adjusted", "scaled.shifted",
      "mean.var.adjusted.corrected", "scaled.shifted.corrected"
    ))) {
    lav_msg_stop(gettextf(
      "information must be either %s if robust test statistics are requested.",
      lav_msg_view(c("expected", "observed"), log_sep = "or")
    ))
  }


  if (length(opt$observed.information) == 1L) {
    opt$observed.information <- rep(opt$observed.information, 2L)
  }

  if (all(opt$observed.information[2] != c("hessian", "h1"))) {
    if (opt$observed.information[2] == "default") {
      if (any(opt$test %in% c(
        "satorra.bentler",
        "yuan.bentler",
        "yuan.bentler.mplus",
        "mean.var.adjusted",
        "mean.var.adjusted.corrected",
        "scaled.shifted",
        "scaled.shifted.corrected"
      ))) {
        if (length(opt$test) > 1L) {
          opt$observed.information[2] <- "h1" # CHANGED in 0.6-6!
          if (any(opt$test == "yuan.bentler.mplus")) {
            lav_msg_warn(gettext(
              "observed.information for ALL test statistics is set to h1."
            ))
          }
        } else {
          if (opt$estimator == "PML" ||
            opt$test[1] == "yuan.bentler.mplus") {
            opt$observed.information[2] <- "hessian"
          } else {
            opt$observed.information[2] <- "h1" # CHANGED in 0.6-6!
          }
        }
      } else {
        # default is "hessian"
        opt$observed.information[2] <- "hessian"
      }
    }
  }

  if (length(opt$h1.information) == 1L) {
    opt$h1.information <- rep(opt$h1.information, 2L)
  }

  if (opt$h1.information.meat == "default") {
    opt$h1.information.meat <- opt$h1.information[1]
  }

  # check information if estimator is uls/wls and friends
  if (any(lav_options_estimatorgroup(opt$estimator) ==
    c("ULS", "WLS", "DWLS"))) {
    #if (opt$information[1] != "expected") {
    #  lav_msg_warn(gettextf(
    #    "information will be set to %1$s for estimator = %2$s",
    #    dQuote("expected"), dQuote(opt$estimator)
    #  ))
    #  opt$information <- rep.int("expected", 2L)
    #}
    opt$h1.information <- rep.int("unstructured", 2L) # should always be fixed
  }


  # omega information
  if (opt$omega.information == "default") {
    opt$omega.information <- opt$information[2] # test version!
  }

  if (opt$omega.h1.information == "default") {
    # opt$omega.h1.information <- opt$h1.information[2] # test version!
    opt$omega.h1.information <- "unstructured"
  }

  if (opt$omega.h1.information.meat == "default") {
    opt$omega.h1.information.meat <- opt$omega.h1.information
  }

  # conditional.x
  if (is.character(opt$conditional.x)) { # = "default"
    if (opt$.categorical) {
      opt$conditional.x <- TRUE
    } else {
      opt$conditional.x <- FALSE
    }
  }

  # if conditional.x, always use a meanstructure
  if (opt$conditional.x) {
    opt$meanstructure <- TRUE
    # auto.cov.x has no effect if conditional.x = TRUE: the exogenous
    # covariates are conditioned out, so their covariances with exogenous
    # latent variables cannot be represented
    opt$auto.cov.x <- FALSE
  }

  # fixed.x
  if (is.logical(opt$fixed.x)) {
    # if(opt$conditional.x && opt$fixed.x == FALSE && !opt$.multilevel) {
    if (opt$conditional.x && opt$fixed.x == FALSE) {
      lav_msg_stop(gettext(
        "fixed.x = FALSE is not supported when conditional.x = TRUE."
      ))
    }
    if (opt$fixed.x && is.character(opt$start) && opt$start == "simple") {
      lav_msg_warn(gettextf(
        "start = %s implies fixed.x = FALSE", dQuote(opt$start)
      ))
      opt$fixed.x <- FALSE
    }
  } else if (opt$fixed.x == "default") {
    if (opt$conditional.x) {
      opt$fixed.x <- TRUE
    } else {
      opt$fixed.x <- FALSE
    }
  }

  # meanstructure again
  if (any(opt$missing == c("ml", "ml.x")) || opt$model.type == "growth") {
    opt$meanstructure <- TRUE
  }
  if (any(c("intercepts", "means") %in% opt$group.equal)) {
    opt$meanstructure <- TRUE
  }
  # if(opt$se == "robust.huber.white" ||
  #   opt$se == "robust.sem" ||
  #   opt$test == "satorra.bentler" ||
  #   opt$test == "mean.var.adjusted" ||
  #   opt$test == "scaled.shifted" ||
  #   opt$test == "yuan.bentler") {
  #    opt$meanstructure <- TRUE
  # }
  if (!is.logical(opt$meanstructure)) {
    lav_msg_fixme("meanstructure must be logical at this point!")
  }

  # zero cell frequencies
  if (is.character(opt$zero.add)) { # = "default"
    opt$zero.add <- c(0.5, 0.0)
    # FIXME: TODO: mimic EQS , LISREL (0.0, 0.0)
  } else {
    if (length(opt$zero.add) == 1L) {
      opt$zero.add <- c(opt$zero.add, opt$zero.add)
    }
  }

  if (is.character(opt$zero.keep.margins)) { # = "default"
    opt$zero.keep.margins <- FALSE
  }


  # parameterization
  if (opt$parameterization == "default") {
    # for now, default is always delta
    opt$parameterization <- "delta"
  }

  # std.lv vs auto.fix.first # new in 0.6-5 (used to be in sem/cfa/growth)
  if (opt$std.lv) {
    opt$auto.fix.first <- FALSE
  }

  # std.lv vs effect.coding # new in 0.6-4
  if (is.logical(opt$effect.coding)) {
    if (opt$effect.coding) {
      opt$effect.coding <- c("loadings", "intercepts")
    } else {
      opt$effect.coding <- ""
    }
  }

  # if we use effect coding for the factor loadings, we don't need/want
  # std.lv = TRUE
  if (any("loadings" == opt$effect.coding)) {
    if (opt$std.lv) {
      lav_msg_stop(gettextf(
        "std.lv is set to FALSE but effect.coding contains %s",
        dQuote("loadings")
      ))
    }
    # shut off auto.fix.first
    opt$auto.fix.first <- FALSE
  }

  # test again

  # unless test = "none", always add test = "standard" as the
  # first entry
  # NO: this breaks lavaan.survey pval.pFsum, which has the following check:
  # if (!lavInspect(lavaan.fit, "options")$test %in% c("satorra.bentler",
  #    "mean.var.adjusted", "Satterthwaite")) {
  #    lav_msg_stop(
  #    gettext("Please refit the model with Satorra-Bentler (MLM)"),
  #    gettext(" or Satterthwaite (MLMVS) adjustment."))
  # }
  # if(! (length(opt$test) == 1L && opt$test == "none") ) {
  #    opt$test <- c("standard", opt$test)
  #    opt$test <- unique(opt$test)
  # }

  # add standard.test to test (if not already there)
  if (opt$standard.test != "standard") {
    if (length(opt$test) == 1L && opt$test[1] == "standard") {
      opt$test <- unique(c(opt$test, opt$standard.test))
    } else {
      opt$test <- unique(c(opt$standard.test, opt$test))
    }
  }

  # add scaled.test to test (if not already there)
  if (opt$scaled.test != "standard") {
    if (length(opt$test) == 1L && opt$test[1] == "standard") {
      opt$test <- unique(c(opt$test, opt$scaled.test))
    } else {
      opt$test <- unique(c(opt$scaled.test, opt$test))
    }
  }

  # where does "standard" appear in the opt$test vector?
  if (opt$test[1] != "none") {
    standard_idx <- which(opt$test == "standard")[1]
    if (is.na(standard_idx)) {
      # "standard" is not in the opt$test vector at all,
      # so add it
      opt$test <- c("standard", opt$test)
    } else if (length(standard_idx) > 0L && standard_idx != 1L) {
      # make sure "standard" comes first
      opt$test <- c("standard", opt$test[-standard_idx])
    }
  }

  # final check
  wrong_idx <- which(!opt$test %in% c(
    "none", "standard", "satorra.bentler",
    "yuan.bentler", "yuan.bentler.mplus",
    "yuan.chan", # SAM-only (sam.method = "global"); ignored elsewhere
    "mean.var.adjusted", "scaled.shifted",
    "mean.var.adjusted.corrected", "scaled.shifted.corrected",
    "browne.residual.adf", "browne.residual.nt",
    "browne.residual.nt.model",
    "browne.residual.adf.model",
    "bollen.stine"
  ))
  fmg_idx <- which(vapply(opt$test, lav_test_fmg_is_fmg, logical(1L)))
  wrong_idx <- setdiff(wrong_idx, fmg_idx)
  if (length(wrong_idx) > 0L) {
    lav_msg_stop(gettextf(
      "invalid option(s) for test argument: %1$s. Possible options are: %2$s.",
      lav_msg_view(opt$test[wrong_idx]),
      lav_msg_view(c(
        "none", "standard", "browne.residual.adf",
        "browne.residual.nt", "browne.residual.adf.model",
        "browne.residual.nt.model", "satorra.bentler",
        "yuan.bentler", "yuan.bentler.mplus",
        "mean.var.adjusted", "scaled.shifted",
        "mean.var.adjusted.corrected", "scaled.shifted.corrected",
        "bollen.stine"
      ), log_sep = "or")
    ))
  }

  # bounds
  if (is.null(opt$bounds)) {
    if (length(opt$optim.bounds) > 0L) {
      opt$bounds <- "user"
    } else {
      opt$bounds <- "none" # for now
    }
  } else if (is.logical(opt$bounds)) {
    if (opt$bounds) {
      opt$bounds <- "wide" # default for most estimators
    } else {
      opt$bounds <- "none"
    }
  }

  # optim.bounds
  if (length(opt$optim.bounds) > 0L) {
    # opt$bounds should be "default", or "user" (or "none")
    if (any(opt$bounds == c("default", "none", "user"))) {
      opt$bounds <- "user"
    } else {
      lav_msg_stop(
        gettext("bounds and optim.bounds arguments can not be used together;
                remove the bounds= argument or set it to \"user\".")
      )
    }
  }

  # handle different 'profiles'
  if (opt$bounds == "none") {
    opt$optim.bounds <- list(
      lower = character(0L),
      upper = character(0L)
    )
  } else if (opt$bounds == "user") {
    if (length(opt$optim.bounds) == 0L) {
      lav_msg_stop(gettextf(
        "bounds= is %s but optim.bounds= argument is empty", dQuote("user")
      ))
    }
  } else if (opt$bounds == "default" || opt$bounds == "wide") {
    opt$optim.bounds <- list(
      lower = c("ov.var", "lv.var", "loadings", "covariances"),
      upper = c("ov.var", "lv.var", "loadings", "covariances"),
      lower.factor = c(1.05, 1.0, 1.1, 1.0),
      upper.factor = c(1.20, 1.3, 1.1, 1.0),
      min.reliability.marker = 0.1,
      min.var.lv.endo = 0.005
    )
  } else if (opt$bounds == "wide.zerovar") {
    opt$optim.bounds <- list(
      lower = c("ov.var", "lv.var", "loadings", "covariances"),
      upper = c("ov.var", "lv.var", "loadings", "covariances"),
      lower.factor = c(1.00, 1.0, 1.1, 1.0),
      upper.factor = c(1.20, 1.3, 1.1, 1.0),
      min.reliability.marker = 0.1,
      min.var.lv.endo = 0.005
    )
  } else if (opt$bounds == "standard") {
    opt$optim.bounds <- list(
      lower = c("ov.var", "lv.var", "loadings", "covariances"),
      upper = c("ov.var", "lv.var", "loadings", "covariances"),
      lower.factor = c(1.0, 1.0, 1.0, 0.999),
      upper.factor = c(1.0, 1.0, 1.0, 0.999),
      min.reliability.marker = 0.1,
      min.var.lv.endo = 0.005
    )
  } else if (opt$bounds == "pos.var") {
    opt$optim.bounds <- list(
      lower = c("ov.var", "lv.var"),
      lower.factor = c(1, 1),
      min.reliability.marker = 0.0,
      min.var.lv.exo = 0.0,
      min.var.lv.endo = 0.0
    )
  } else if (opt$bounds == "pos.ov.var") {
    opt$optim.bounds <- list(
      lower = c("ov.var"),
      lower.factor = 1
    )
  } else if (opt$bounds == "pos.lv.var") {
    opt$optim.bounds <- list(
      lower = c("lv.var"),
      lower.factor = 1,
      min.reliability.marker = 0.0,
      min.var.lv.exo = 0.0,
      min.var.lv.endo = 0.0
    )
  }

  # rotations.args
  if (!is.list(opt$rotation.args)) {
    lav_msg_stop(gettext("rotation.args should be a list."))
  }

  # transform names rotation.args to snake_case
  opt$rotation.args <- lav_snake_case(opt$rotation.args)

  # force orthogonal for some rotation algorithms
  if (any(opt$rotation == c(
    "varimax", "entropy", "mccammon",
    "tandem1", "tandem2"
  ))) {
    opt$rotation.args$orthogonal <- TRUE
  }

  # if target, check target matrix, and set order.lv.by to = "none"
  if (opt$rotation == "target.strict" || opt$rotation == "pst") {
    target <- opt$rotation.args$target
    if (is.null(target)) {
      lav_msg_stop(gettext("rotation target matrix is NULL"))
    }
    if (is.list(target)) {
      if (!all(sapply(target, is.matrix))) {
        lav_msg_stop(gettext("the target list contains
                              elements that are not a matrix"))
      }
    } else if (!is.matrix(target)) {
      lav_msg_stop(gettext("rotation target matrix is not a matrix"))
    }
    opt$rotation.args$order_lv_by <- "none"
  }

  if (opt$rotation == "pst") {
    target_mask <- opt$rotation.args$target_mask
    if (is.null(target_mask) || length(target_mask) == 0L) {
      # lav_msg_stop(gettext("rotation target_mask matrix is NULL"))
      if (is.matrix(target)) {
        tmp <- matrix(1L, nrow = nrow(target), ncol = ncol(target))
        tmp[target != 0] <- 0L # ignore these (non-zero) elements
        opt$rotation.args$target_mask <- target_mask <- tmp
      } else if (is.list(target)) {
        # lapply preserves the names (if any) of the target list
        out <- lapply(target, function(tt) {
          tmp <- matrix(1L, nrow = nrow(tt), ncol = ncol(tt))
          tmp[tt != 0] <- 0L # ignore these (non-zero) elements
          tmp
        })
        opt$rotation.args$target_mask <- target_mask <- out
      }
    }
    if (is.list(target_mask)) {
      if (!all(sapply(target_mask, is.matrix))) {
        lav_msg_stop(gettext("the target_mask list contains
                              elements that are not a matrix"))
      }
    } else if (!is.matrix(target_mask)) {
      lav_msg_stop(gettext("rotation target_mask matrix is not a matrix"))
    }
    if (is.list(target) && !is.list(target_mask)) {
      lav_msg_stop(gettext("target is a list, but target_mask is not a list"))
    }
    if (is.list(target_mask) && !is.list(target)) {
      lav_msg_stop(gettext("target_mask is a list, but target is not a list"))
    }
    if (is.list(target) && is.list(target_mask)) {
      if (length(target) != length(target_mask)) {
        lav_msg_stop(gettext("length(target) != length(target_mask)"))
      }
      # a *named* list is keyed by the efa block labels; a *nameless* list
      # is interpreted per group -- both lists must use the same convention
      target_named <- !is.null(names(target)) && any(nzchar(names(target)))
      mask_named <- !is.null(names(target_mask)) &&
        any(nzchar(names(target_mask)))
      if (target_named != mask_named) {
        lav_msg_stop(gettext(
          "target and target_mask must either both be named lists (with the efa block labels as names), or both be nameless lists (one element per group)."))
      }
      if (target_named && !setequal(names(target), names(target_mask))) {
        lav_msg_stop(gettext(
          "the names of the target list do not match the names of the target_mask list."))
      }
    }
  }

  # if NAs, force opt$rotation to be 'pst' and create target_mask
  if (opt$rotation == "target.strict") {
    # matrix
    warn_flag <- FALSE
    if (is.matrix(target) && anyNA(target)) {
      warn_flag <- TRUE
      opt$rotation <- "pst"
      target_mask <- matrix(1, nrow = nrow(target), ncol = ncol(target))
      target_mask[is.na(target)] <- 0
      opt$rotation.args$target_mask <- target_mask

      # list
    } else if (is.list(target) && any(sapply(target, anyNA))) {
      warn_flag <- TRUE
      opt$rotation <- "pst"
      # create a mask for *every* element (all-ones if no NA values);
      # lapply preserves the names (if any) of the target list
      opt$rotation.args$target_mask <- lapply(target, function(tt) {
        tmp <- matrix(1, nrow = nrow(tt), ncol = ncol(tt))
        tmp[is.na(tt)] <- 0
        tmp
      })
    }
    if (warn_flag) {
      lav_msg_warn(gettext(
        "switching to PST rotation as target matrix contains NA values"
      ))
    }
  }

  # set row_weights
  opt$rotation.args$row_weights <- tolower(opt$rotation.args$row_weights)
  if (opt$rotation.args$row_weights == "default") {
    # the default is "none", except for varimax and promax
    if (any(opt$rotation == c("varimax", "promax"))) {
      opt$rotation.args$row_weights <- "kaiser"
    } else {
      opt$rotation.args$row_weights <- "none"
    }
  }

  # override if bifactor
  if (any(opt$rotation == c(
    "bi-geomin", "bigeomin", "bi-quartimin",
    "biquartimin"
  ))) {
    opt$rotation.args$order_lv_by <- "none"
  }

  # no standard errors for promax (for now)...
  if (opt$rotation == "promax") {
    opt$se <- "none"
    opt$rotation.args$algorithm <- "promax"
    opt$rotation.args$rstarts <- 0L
  }

  # correlation
  if (opt$correlation) {
    # standardize
    opt$std.ov <- TRUE
    # if ML, switch to GLS
    if (opt$estimator == "ml") {
      # lav_msg_warn(gettext(
      #        "GLS should be used for correlation structures instead of ML."))
      opt$estimator <- "gls"
    }
    if (opt$missing == "ml") {
      lav_msg_stop(gettext(
        "correlation structures only work for complete data (for now)."
      ))
    }
    if (opt$.multilevel) {
      lav_msg_stop(gettext(
        "correlation structures only work for single-level data."
      ))
    }
    if (opt$conditional.x) {
      lav_msg_stop(gettext(
        "correlation structures only work for conditional.x = FALSE (for now)."
      ))
    }
    if (opt$representation == "RAM") {
      lav_msg_stop(gettext(
        "correlation structures only work for representation = \"LISREL\"."
      ))
    }
  }

  # sample.cov.robust
  # new in 0.6-17
  # sample.cov.robust cannot be used if:
  # - data is missing (for now),
  # - sampling weights are used
  # - estimator is (D)WLS
  # - multilevel
  # - conditional.x
  if (opt$sample.cov.robust) {
    if (opt$missing != "listwise") {
      lav_msg_stop(gettext(
        "sample.cov.robust = TRUE does not work (yet) if data is missing."
      ))
    }
    if (opt$.categorical) {
      lav_msg_stop(gettext(
        "sample.cov.robust = TRUE does not work (yet) if data is categorical"
      ))
    }
    if (opt$.clustered || opt$.multilevel) {
      lav_msg_stop(gettext(
        "sample.cov.robust = TRUE does not work (yet) if data is clustered"
      ))
    }
    if (opt$conditional.x) {
      lav_msg_stop(gettext(
        "sample.cov.robust = TRUE does not work (yet) if conditional.x = TRUE"
      ))
    }
    if (all(lav_options_estimatorgroup(opt$estimator) != c("ML", "GLS"))) {
      lav_msg_stop(gettext(
        "sample.cov.robust = TRUE does not work (yet)
        if estimator is not GLS or ML"
      ))
    }
  }

  opt$estimator <- lav_options_estimatorgroup(opt$estimator)

  # group.w.free
  # if(opt$group.w.free && opt$.categorical) {
  #    lav_msg_stop(gettext(
  #    "group.w.free = TRUE is not supported (yet) in the categorical setting."
  #    ))
  # }

  # in order not to break semTools and blavaan, we restore categorical:
  opt$categorical <- opt$.categorical

  if (lav_debug()) {
    cat("lavaan DEBUG: lavaanOptions OUT\n")
    str(opt)
  }

  opt
}
