write_json <- function(
  file,
  x,
  ...,
  pretty = TRUE,
  auto_unbox = TRUE,
  generated_by = "scripts/htmlDependencies.R"
) {
  jsonlite::write_json(
    c(
      list(
        "note!" = sprintf("Generated by %s: do not edit by hand", generated_by)
      ),
      x
    ),
    file,
    ...,
    pretty = pretty,
    auto_unbox = auto_unbox
  )
}

copy_from_pkg <- function(pkg_name, pkg_dir, local_dir, version_dir = path_dir(local_dir)) {
  # `version_dir` is "equal to" or "contains" `local_dir`
  stopifnot(path_has_parent(local_dir, version_dir))
  if (dir_exists(version_dir)) dir_delete(version_dir)
  dir_create(local_dir)

  stopifnot(local_dir != ".")

  cli::cli_progress_step("Copy {.strong {pkg_dir}} from {.pkg {pkg_name}} to {.path {path_rel(local_dir)}}")

  # Copy other folder into local parent folder
  dir_copy(
    system.file(pkg_dir, package = pkg_name),
    dirname(local_dir)
  )
  # Rename folder to local folder name
  if (basename(local_dir) != basename(pkg_dir)) {
    file.rename(
      path(dirname(local_dir), basename(pkg_dir)),
      local_dir
    )
  }
  # Save pkg version info
  write_json(
    path(version_dir, "_version.json"),
    list(
      package = pkg_name,
      version = pkg_source_version(pkg_name)
    )
  )

  invisible(local_dir)
}

local_sass_options <- withr::local_(function(x) rlang::exec(sass::sass_options_set, !!!x))

local_sass_compressed <- function(.envir = parent.frame()) {
  local_sass_options(
    list(
      output_style = "compressed",
      source_comments = FALSE,
      source_map_embed = FALSE
    ),
    .local_envir = .envir
  )
}

delete_non_minified <- function(dir) {
  file_delete(
    dir_ls(
      dir,
      type = "file",
      recurse = TRUE,
      regexp = "\\.(min\\.|css)",
      invert = TRUE
    )
  )
}

cli_progress_with_bs_theme <- function(message, theme, .envir = parent.frame()) {
  info <- bslib:::theme_preset_info(theme)
  suffix <- sprintf(
    "with BS%s (preset: %s)",
    info$version,
    info$name
  )

  cli::cli_progress_step(
    paste(message, suffix),
    .envir = .envir
  )
}

write_deps_ionrangeslider <- function(theme, www_shared) {
  cli_progress_with_bs_theme("Render {.field ionRangeSlider} CSS", theme)
  preset <- bslib:::theme_preset_info(theme)$name
  local_sass_compressed()

  ion_dep <- shiny:::ionRangeSliderDependencyCSS(theme)
  ion_dep_css <- path(ion_dep$src$file, ion_dep$stylesheet)

  # Preset="shiny" additional ionRangeSlider rules
  if (identical(preset, "shiny")) {
    ion_preset_rules <- system.file("builtin", "bs5", "shiny", "ionrangeslider", "_rules.scss", package = "bslib")


    ion_preset_compiled <-
      sass::sass_partial(
        readLines(ion_preset_rules),
        theme,
        write_attachments = FALSE,
        cache = FALSE
      )

    # Append preset styles to the base ionRangeSlider CSS
    cat(
      c("\n\n/* shiny preset styles */\n\n", ion_preset_compiled),
      file = ion_dep_css,
      sep = "\n",
      append = TRUE
    )
  }

  if (inherits(ion_dep, "html_dependency")) {
    ion_dep <- list(ion_dep)
  }

  # Save to temp folder
  temp_ion_dep_dir <- withr::local_tempdir("shiny-ion-range-slider")

  withr::with_options(
    list(htmltools.dir.version = FALSE),
    ignore <- lapply(ion_dep, htmltools::copyDependencyToDir, temp_ion_dep_dir)
  )

  # Overwrite css file
  ion_dep_dir <- path(www_shared, "ionrangeslider")
  ion_dep_css <- path(ion_dep_dir, "css", "ion.rangeSlider.css")

  file_move(
    path(temp_ion_dep_dir, "ionRangeSlider", "ionRangeSlider.min.css"),
    ion_dep_css
  )

  invisible(ion_dep_css)
}

write_bootstrap_bslib_deps <- function(theme, www_shared) {
  cli_progress_with_bs_theme("Render {.field bslib} component CSS", theme)
  local_sass_compressed()

  deps <- bslib::bs_theme_dependencies(theme)

  withr::with_options(
    list(htmltools.dir.version = FALSE),
    for (dep in deps) {
      if (dep$name %in% c("bslib-component-css", "bslib-component-js")) {
        next
      }
      copyDependencyToDir(dep, www_shared)
    }
  )

  write_json(
    path(www_shared, "bootstrap/_version.json"),
    list(
      shiny_version = pkg_source_version("shiny"),
      bslib_version = pkg_source_version("bslib"),
      htmltools_version = pkg_source_version("htmltools"),
      bootstrap_version = bs_version_full(bslib::theme_version(theme))
    )
  )

  path_bootstrap <- path(www_shared, "bootstrap")
  path_bs_fonts <- path(path_bootstrap, "font.css")

  if (!file_exists(path_bs_fonts)) {
    return(invisible())
  }

  cli::cli_progress_step("Remove unused font files")

  font_css_text <- paste(readLines(path_bs_fonts), collapse = "\n")

  woff_files <- dir_ls(path(path_bootstrap, "fonts"), regexp = "[.]woff")

  for (woff_file in woff_files) {
    font_file_name <- path_file(woff_file)
    if (!grepl(font_file_name, font_css_text, fixed = TRUE)) {
      file_delete(woff_file)
    }
  }

  invisible()
}

extract_css_path <- function(dep) {
  stopifnot(inherits(dep, "html_dependency"))
  path(dep$src$file, dep$stylesheet)
}

write_shiny_css <- function(theme, www_shared) {
  cli_progress_with_bs_theme("Render {.field shiny} CSS", theme)
  local_sass_compressed()

  shiny_css_dep <- shiny:::shinyDependencyCSS(theme)
  shiny_css_bslib <- extract_css_path(shiny_css_dep)
  shiny_css_shared <- path(www_shared, "shiny.min.css")
  writeLines(
    c(
      readLines(shiny_css_shared, n = 1), # keep header of original minified css
      readLines(shiny_css_bslib)
    ),
    con = shiny_css_shared
  )

  invisible(shiny_css_shared)
}

write_selectize_css <- function(theme, www_shared) {
  cli_progress_with_bs_theme("Render {.field selectize} CSS", theme)
  local_sass_compressed()

  selectize_css_dep <- shiny:::selectizeDependencyFunc(theme)
  selectize_css_bslib <- extract_css_path(selectize_css_dep)
  selectize_shared <- path(www_shared, "selectize", "css")

  file_delete(path(selectize_shared, "selectize.bootstrap3.css"))

  file_copy(
    selectize_css_bslib,
    path(selectize_shared, "selectize.min.css"),
    overwrite = TRUE
  )

  invisible(path(selectize_shared, "selectize.min.css"))
}

write_datepicker_css <- function(theme, www_shared) {
  cli_progress_with_bs_theme("Render {.field datepicker} CSS", theme)
  local_sass_compressed()

  datepicker_css_dep <- shiny:::datePickerCSS(theme)
  datepicker_css_bslib <- extract_css_path(datepicker_css_dep)
  datepicker_shared <- path(www_shared, "datepicker", "css")

  file_copy(
    datepicker_css_bslib,
    path(datepicker_shared, "bootstrap-datepicker3.min.css"),
    overwrite = TRUE
  )

  invisible(path(datepicker_shared, "bootstrap-datepicker3.min.css"))
}

delete_from_www_shared <- function(www_shared, ...) {
  files <- path(www_shared, c(...))

  dirs <- files[is_dir(files)]
  files <- files[!is_dir(files)]

  file_delete(files)
  dir_delete(dirs)
}

write_require_js <- function(requirejs_version, www_shared) {
  requirejs <- path(www_shared, "requirejs")
  cli::cli_progress_step("Download {.field require.js} to {.path {path_rel(requirejs)}}")

  dir_create(requirejs)

  download.file(
    paste0("https://cdnjs.cloudflare.com/ajax/libs/require.js/", requirejs_version, "/require.min.js"),
    path(requirejs, "require.min.js"),
    quiet = TRUE
  )

  shims <- path_root("scripts", "define-shims.js")

  cat(
    "\n\n",
    paste(readLines(shims), collapse = "\n"),
    file = path(requirejs, "require.min.js"),
    append = TRUE
  )

  invisible(path(requirejs, "require.min.js"))
}

write_versions_py <- function(bootstrap, requirejs) {
  path_versions_py <- path_root("shiny", "_versions.py")
  cli::cli_progress_step("Write versions to {.path {path_rel(path_versions_py)}}")

  versions <- list(
    shiny_html_deps = as.character(packageVersion("shiny")),
    bslib = as.character(packageVersion("bslib")),
    htmltools = as.character(packageVersion("htmltools")),
    bootstrap = bs_version_full(bootstrap),
    requirejs = requirejs
  )


  version_vars <- paste0(names(versions), " = ", "\"", versions, "\"\n", collapse = "")
  version_all <- paste0(
    collapse = "",
    "__all__ = (\n",
    paste0("    \"", names(versions), "\",\n", collapse = ""),
    ")\n"
  )

  cat(
    file = path_versions_py,
    version_vars,
    "\n",
    version_all,
    sep = ""
  )

  invisible(path_versions_py)
}

npm_install_dependencies <- function() {
  cli::cli_progress_step("Install npm dependencies")
  withr::local_dir(path_root("js"))
  invisible(system("npm install && npm run build", intern = TRUE, ignore.stderr = TRUE))
}

write_spinners_py <- function(www_shared) {
  path_busy_spinner_types_py <- path_root("shiny", "ui", "_busy_spinner_types.py")
  cli::cli_progress_step("Write spinner types to {.path {path_rel(path_busy_spinner_types_py)}}")

  spinner_types <- dir_ls(
    path(www_shared, "busy-indicators", "spinners"),
    regexp = "[.]svg$"
  )
  spinner_types <- path_ext_remove(path_file(spinner_types))

  template <- r"(# Generated by scripts/htmlDependencies.R: do not edit by hand

from __future__ import annotations

from typing import Literal

BusySpinnerType = Literal[
%s
])"

  py_lines <- function(x) {
    x <- paste(sprintf('"%s"', x), collapse = ",\n    ")
    paste0("    ", x, ",")
  }


  writeLines(
    sprintf(template, py_lines(spinner_types)),
    path_busy_spinner_types_py
  )

  invisible(path_busy_spinner_types_py)
}
