## _helpers.R -- sourced by every session walkthrough.
## Kept tiny and dependency-free on purpose: a remote student must be able to
## read the whole file in 60 seconds and trust it.

## ---------------------------------------------------------------------------
## fit_or_load(): the single most important idiom in these documents.
##
## knitr's `cache: true` only helps the person RENDERING. A student stepping
## through chunks interactively in RStudio gets no benefit from it. fit_or_load
## ships a fitted object alongside the .qmd, so the student's interactive run is
## as fast as the instructor's, and `refit = TRUE` lets them reproduce it for
## real when they have the time.
## ---------------------------------------------------------------------------
fit_or_load <- function(name, expr, dir = "cache", refit = getOption("session.refit", FALSE)) {
  f <- file.path(dir, paste0(name, ".rds"))
  if (!refit && file.exists(f)) {
    message("[cached] ", name, "  (set options(session.refit = TRUE) to refit)")
    return(readRDS(f))
  }
  dir.create(dir, showWarnings = FALSE, recursive = TRUE)
  t0  <- Sys.time()
  ## siena07(batch = TRUE) writes its Phase 1/2/3 iteration progress to stdout,
  ## which knitr's `message: false` / `warning: false` do NOT catch (it is not a
  ## condition). Swallow it so a from-scratch render or a student refit does not
  ## bury the interpretive prose under hundreds of "Phase 2 Iteration N" lines.
  utils::capture.output(obj <- force(expr))
  message(sprintf("[fitted] %s in %.1fs", name,
                  as.numeric(difftime(Sys.time(), t0, units = "secs"))))
  saveRDS(obj, f)
  obj
}

## ---------------------------------------------------------------------------
## checkpoint(): prints a compact, comparable line. The instructor shows the
## same line on the projector. A remote student who sees a different number
## knows immediately -- and knows it before the next section depends on it.
## ---------------------------------------------------------------------------
checkpoint <- function(..., label = "CHECKPOINT") {
  vals <- list(...)
  nms  <- names(vals)
  if (is.null(nms)) nms <- rep("", length(vals))
  body <- paste(mapply(function(n, v) {
    v <- if (is.numeric(v)) format(round(v, 3), nsmall = 0) else as.character(v)
    if (nzchar(n)) paste0(n, " = ", paste(v, collapse = ", ")) else paste(v, collapse = ", ")
  }, nms, vals), collapse = "   |   ")
  cat(strrep("-", 66), "\n", label, ": ", body, "\n", strrep("-", 66), "\n", sep = "")
  invisible(TRUE)
}

## ---------------------------------------------------------------------------
## require_pkgs(): fails loudly and early with an actionable message, instead of
## dying 40 minutes in. Never installs anything without being asked.
## ---------------------------------------------------------------------------
require_pkgs <- function(pkgs) {
  missing <- pkgs[!vapply(pkgs, requireNamespace, logical(1), quietly = TRUE)]
  if (length(missing)) {
    stop("Missing package(s): ", paste(missing, collapse = ", "),
         "\nRun:  install.packages(c(", paste0('"', missing, '"', collapse = ", "), "))",
         call. = FALSE)
  }
  invisible(TRUE)
}

## ---------------------------------------------------------------------------
## Silence the cosmetic glpk warning that ergm emits on every MCMLE fit when
## Rglpk is absent. 37 students should not see a scary warning on every model.
## Prefer actually installing Rglpk; this is the fallback.
## ---------------------------------------------------------------------------
quiet_solver <- function() {
  if (!requireNamespace("Rglpk", quietly = TRUE)) {
    options(ergm.loglik.warn_dyads = FALSE)
  }
  invisible(TRUE)
}
