# Build _remark-offline/ for classroom playback without a network.
# The bundle mirrors repo-relative paths used by *-offline.html.
# Deck list is assets-remarkjs/decks.csv (see decks.example.csv).
# Run from the project root:
#   source("assets-remarkjs/pack-offline.R")
#   pack_remark_offline()

read_remark_decks <- function(path) {
  if (!file.exists(path)) {
    stop("缺少 decks.csv：", path, "。可从 decks.example.csv 复制。", call. = FALSE)
  }
  decks <- utils::read.csv(
    path,
    stringsAsFactors = FALSE,
    fileEncoding = "UTF-8",
    check.names = FALSE
  )
  need <- c("part", "section", "title", "stem")
  miss <- setdiff(need, names(decks))
  if (length(miss)) {
    stop("decks.csv 缺少列：", paste(miss, collapse = ", "), call. = FALSE)
  }
  if (!nrow(decks)) stop("decks.csv 没有课件行。", call. = FALSE)
  decks
}

pack_remark_offline <- function(decks = NULL, root = here::here()) {
  root <- normalizePath(root, winslash = "/", mustWork = TRUE)
  source(file.path(root, "assets-remarkjs/render-slide.R"), local = TRUE)
  if (is.null(decks)) {
    decks <- read_remark_decks(file.path(root, "assets-remarkjs/decks.csv"))
  }

  dest <- file.path(root, "_remark-offline")
  if (dir.exists(dest)) unlink(dest, recursive = TRUE)
  if (dir.exists(dest)) stop("无法清空：", dest)
  dir.create(dest, recursive = TRUE)

  for (i in seq_len(nrow(decks))) {
    input <- file.path(root, decks$part[i], "slide-xaringan", paste0(decks$stem[i], ".Rmd"))
    message("offline render ", i, "/", nrow(decks), ": ", input)
    render_xaringan_slide(input, "offline")
  }

  copy_pack_assets(root, dest, decks)
  write_pack_index(dest, decks)
  write_pack_bat(dest)
  validate_remark_pack(dest, decks)
  message("PACK_OK ", dest)
  invisible(dest)
}

copy_pack_file <- function(from, to) {
  if (!file.exists(from)) stop("缺少文件：", from)
  dir.create(dirname(to), recursive = TRUE, showWarnings = FALSE)
  ok <- file.copy(from, to, overwrite = TRUE, copy.date = TRUE)
  if (!all(ok)) stop("复制失败：", from)
}

copy_pack_tree <- function(from, to_parent) {
  if (!dir.exists(from)) stop("缺少目录：", from)
  dir.create(to_parent, recursive = TRUE, showWarnings = FALSE)
  ok <- file.copy(from, to_parent, recursive = TRUE, copy.date = TRUE)
  if (!all(ok)) stop("复制失败：", from)
}

copy_pack_assets <- function(root, dest, decks) {
  copy_pack_file(
    file.path(root, "assets-remarkjs/remark-latest.min.js"),
    file.path(dest, "assets-remarkjs/remark-latest.min.js")
  )
  copy_pack_file(
    file.path(root, "assets-remarkjs/mathjax-config.js"),
    file.path(dest, "assets-remarkjs/mathjax-config.js")
  )
  copy_pack_file(
    file.path(root, "assets-remarkjs/xaringan-math-spacing.css"),
    file.path(dest, "assets-remarkjs/xaringan-math-spacing.css")
  )
  copy_pack_tree(file.path(root, "assets-remarkjs/fonts"), file.path(dest, "assets-remarkjs"))
  copy_pack_tree(file.path(root, "vendor/mathjax"), file.path(dest, "vendor"))

  parts <- unique(decks$part)
  for (part in parts) {
    copy_pack_tree(file.path(root, part, "mycss"), file.path(dest, part))
    copy_pack_tree(file.path(root, part, "pic"), file.path(dest, part))
    html_dir <- file.path(dest, part, "slide-xaringan")
    dir.create(html_dir, recursive = TRUE, showWarnings = FALSE)
    copy_pack_tree(
      file.path(root, part, "slide-xaringan/libs"),
      html_dir
    )
  }

  for (i in seq_len(nrow(decks))) {
    part <- decks$part[i]
    stem <- decks$stem[i]
    html_name <- paste0(stem, "-offline.html")
    src_html <- file.path(root, part, "slide-xaringan", html_name)
    if (!file.exists(src_html)) stop("缺少离线 HTML：", src_html)
    copy_pack_file(src_html, file.path(dest, part, "slide-xaringan", html_name))
    files_dir <- file.path(root, part, "slide-xaringan", paste0(stem, "-offline_files"))
    if (dir.exists(files_dir)) {
      copy_pack_tree(files_dir, file.path(dest, part, "slide-xaringan"))
    }
  }
}

write_utf8 <- function(path, text) {
  con <- file(path, open = "wb")
  on.exit(close(con), add = TRUE)
  writeBin(charToRaw(enc2utf8(text)), con)
}

write_pack_index <- function(dest, decks) {
  sections <- unique(decks$section)
  blocks <- vapply(sections, function(section) {
    rows <- which(decks$section == section)
    items <- paste0(
      "    <li><a href=\"",
      decks$part[rows], "/slide-xaringan/", decks$stem[rows], "-offline.html\">",
      decks$title[rows],
      "</a></li>",
      collapse = "\n"
    )
    paste0("<h2>", section, "</h2>\n<ul>\n", items, "\n</ul>")
  }, character(1))
  html <- paste0(
    "<!DOCTYPE html>\n",
    "<html lang=\"zh\">\n",
    "<head>\n",
    "<meta charset=\"utf-8\">\n",
    "<title>离线幻灯片</title>\n",
    "<style>\n",
    "body { font-family: \"Microsoft YaHei\", sans-serif; max-width: 40rem; margin: 2rem auto; line-height: 1.6; }\n",
    "a { color: #003366; }\n",
    "</style>\n",
    "</head>\n",
    "<body>\n",
    "<h1>离线幻灯片</h1>\n",
    "<p>双击本页即可放映。start-offline.bat 只是用默认浏览器打开本页，不需要 Python。</p>\n",
    paste(blocks, collapse = "\n"),
    "\n</body>\n</html>\n"
  )
  write_utf8(file.path(dest, "index.html"), html)
}

write_pack_bat <- function(dest) {
  bat <- paste(
    "@echo off",
    "cd /d \"%~dp0\"",
    "start \"\" \"%~dp0index.html\"",
    "",
    sep = "\r\n"
  )
  write_utf8(file.path(dest, "start-offline.bat"), bat)
}

html_attr_refs <- function(html) {
  match_pos <- gregexpr("(?:href|src)\\s*=\\s*[\"']([^\"']+)[\"']", html, perl = TRUE)[[1]]
  if (identical(match_pos[1], -1L)) return(character())
  starts <- attr(match_pos, "capture.start")[, 1]
  lens <- attr(match_pos, "capture.length")[, 1]
  substring(html, starts, starts + lens - 1L)
}

validate_remark_pack <- function(dest, decks) {
  htmls <- list.files(dest, pattern = "\\.html$", recursive = TRUE, full.names = TRUE)
  slide_html <- htmls[grepl("/slide-xaringan/", htmls, fixed = TRUE)]
  bad_names <- slide_html[!grepl("-offline\\.html$", slide_html)]
  if (length(bad_names)) stop("包内含非离线幻灯片 HTML：", paste(bad_names, collapse = ", "))

  rmd <- list.files(dest, pattern = "\\.Rmd$", recursive = TRUE, full.names = TRUE)
  if (length(rmd)) stop("包内含 Rmd：", paste(rmd, collapse = ", "))

  expected <- file.path(dest, decks$part, "slide-xaringan", paste0(decks$stem, "-offline.html"))
  missing <- expected[!file.exists(expected)]
  if (length(missing)) stop("缺少离线 HTML：", paste(missing, collapse = ", "))

  forbidden <- c("remarkjs.com", "mathjax.rstudio.com", "fonts.googleapis.com")
  for (path in expected) {
    html <- paste(readLines(path, warn = FALSE, encoding = "UTF-8"), collapse = "\n")
    hit <- forbidden[vapply(forbidden, function(x) grepl(x, html, fixed = TRUE), logical(1))]
    if (length(hit)) stop("离线 HTML 仍引用远程资源 ", paste(hit, collapse = ", "), "：", path)

    refs <- html_attr_refs(html)
    for (url in refs) {
      if (!nzchar(url) || grepl("^(?:[a-zA-Z][a-zA-Z0-9+.-]*:|#|//)", url)) next
      url <- sub("[#?].*$", "", url)
      url <- utils::URLdecode(url)
      if (!nzchar(url)) next
      target <- normalizePath(file.path(dirname(path), url), winslash = "/", mustWork = FALSE)
      if (!file.exists(target)) stop("断链 ", url, " ：", path)
    }
  }

  index <- file.path(dest, "index.html")
  index_html <- paste(readLines(index, warn = FALSE, encoding = "UTF-8"), collapse = "\n")
  for (url in html_attr_refs(index_html)) {
    target <- normalizePath(file.path(dest, url), winslash = "/", mustWork = FALSE)
    if (!file.exists(target)) stop("目录页断链：", url)
  }
  if (!file.exists(file.path(dest, "start-offline.bat"))) stop("缺少 start-offline.bat")
  if (!file.exists(file.path(dest, "vendor/mathjax/load-mathjax.js"))) stop("缺少 vendor/mathjax")
  if (!file.exists(file.path(dest, "assets-remarkjs/remark-latest.min.js"))) stop("缺少 remark-latest.min.js")
  if (!file.exists(file.path(dest, "assets-remarkjs/fonts/local-fonts.css"))) stop("缺少本地字体")
  if (!file.exists(file.path(dest, "assets-remarkjs/xaringan-math-spacing.css"))) stop("缺少公式行距 CSS")
}
