// Copyright 2018-2026 the Deno authors. MIT license.

#[cfg(not(feature = "disable"))]
mod shared;

fn main() {
  // Trigger a snapshot rebuild whenever the import-graph instrumentation is
  // toggled (see libs/core/modules/import_graph.rs).
  #[allow(clippy::print_stdout, reason = "build script output")]
  {
    println!("cargo:rerun-if-env-changed=DENO_SNAPSHOT_IMPORT_GRAPH");
  }
  #[cfg(not(feature = "disable"))]
  {
    let o = std::path::PathBuf::from(std::env::var_os("OUT_DIR").unwrap());
    let cli_snapshot_path = o.join("CLI_SNAPSHOT.bin");
    let residual_path = o.join("EXTENSION_RESIDUAL_SOURCES.rs");
    create_cli_snapshot(cli_snapshot_path, residual_path);
  }
}

#[cfg(not(feature = "disable"))]
fn create_cli_snapshot(
  snapshot_path: std::path::PathBuf,
  residual_path: std::path::PathBuf,
) {
  use std::collections::HashSet;
  use std::io::Write;

  use deno_runtime::ops::bootstrap::SnapshotOptions;
  use deno_runtime::snapshot::LazyExtensionFileKind;

  let snapshot_options = SnapshotOptions {
    ts_version: shared::TS_VERSION.to_string(),
    v8_version: deno_runtime::deno_core::v8::VERSION_STRING,
    target: std::env::var("TARGET").unwrap(),
  };

  let output = deno_runtime::snapshot::create_runtime_snapshot(
    snapshot_path,
    snapshot_options,
    vec![],
  );

  let consumed: HashSet<&str> = output
    .consumed_lazy_specifiers
    .iter()
    .map(String::as_str)
    .collect();

  let out_dir = std::path::PathBuf::from(std::env::var_os("OUT_DIR").unwrap());
  let residual_sources_dir = out_dir.join("residual_sources");
  std::fs::create_dir_all(&residual_sources_dir).unwrap();

  let mut residual_js: Vec<(&str, std::path::PathBuf)> = Vec::new();
  let mut residual_esm: Vec<(&str, std::path::PathBuf)> = Vec::new();
  for file in &output.lazy_extension_files {
    if consumed.contains(file.specifier.as_str()) {
      continue;
    }
    // Make sure we rebuild when residual sources change.
    #[allow(clippy::print_stdout, reason = "build script output")]
    {
      println!("cargo:rerun-if-changed={}", file.path.display());
    }
    // Both lazy_loaded_js (loadExtScript -> v8::Script::compile on raw
    // source) and lazy_loaded_esm (op_lazy_load_esm -> LazyEsmModuleLoader,
    // which returns the source unchanged) skip transpilation at runtime.
    // The snapshot path applies extension_transpiler for consumed files;
    // for residuals we have to pre-transpile here so the runtime gets
    // parseable JS rather than TypeScript syntax.
    let transpiled_path = transpile_residual_source(
      &residual_sources_dir,
      &file.specifier,
      &file.path,
    );
    let entry = (file.specifier.as_str(), transpiled_path);
    match file.kind {
      LazyExtensionFileKind::Js => residual_js.push(entry),
      LazyExtensionFileKind::Esm => residual_esm.push(entry),
    }
  }

  let mut f = std::fs::File::create(&residual_path).unwrap();
  writeln!(
    f,
    "// Copyright 2018-2026 the Deno authors. MIT license.\n\
     // @generated by cli/snapshot/build.rs - do not edit.\n"
  )
  .unwrap();
  // Both tables reference files emitted under $OUT_DIR/residual_sources/
  // by `transpile_residual_source`, so `include_str!` uses an OUT_DIR-rooted
  // concat! literal rather than a source-tree path.
  write_residual_table(&mut f, &out_dir, "RESIDUAL_LAZY_JS", &residual_js);
  write_residual_table(&mut f, &out_dir, "RESIDUAL_LAZY_ESM", &residual_esm);
}

#[cfg(not(feature = "disable"))]
fn transpile_residual_source(
  out_dir: &std::path::Path,
  specifier: &str,
  src_path: &std::path::Path,
) -> std::path::PathBuf {
  use deno_runtime::deno_core::ModuleCodeString;
  use deno_runtime::deno_core::ModuleName;
  use deno_runtime::transpile::maybe_transpile_source;

  let source = std::fs::read_to_string(src_path).unwrap_or_else(|e| {
    panic!(
      "failed to read residual lazy source {}: {e}",
      src_path.display()
    )
  });
  let (transpiled, _source_map) = maybe_transpile_source(
    ModuleName::from(specifier.to_string()),
    ModuleCodeString::from(source),
  )
  .unwrap_or_else(|e| {
    panic!("failed to transpile residual lazy source {specifier}: {e}")
  });

  // Sanitize the specifier into a filesystem-safe name. Specifiers look like
  // `ext:deno_node/https.ts`, which we want to round-trip into a unique file.
  let sanitized: String = specifier
    .chars()
    .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
    .collect();
  let out_path = out_dir.join(format!("{sanitized}.js"));
  std::fs::write(&out_path, transpiled.as_bytes()).unwrap();
  out_path
}

#[cfg(not(feature = "disable"))]
fn write_residual_table(
  f: &mut std::fs::File,
  out_dir: &std::path::Path,
  name: &str,
  entries: &[(&str, std::path::PathBuf)],
) {
  use std::io::Write;
  writeln!(f, "pub static {name}: &[(&str, &str)] = &[").unwrap();
  for (specifier, transpiled_path) in entries {
    let rel = transpiled_path.strip_prefix(out_dir).unwrap();
    writeln!(
      f,
      "  ({specifier:?}, include_str!(concat!(env!(\"OUT_DIR\"), {:?}))),",
      format!("/{}", rel.display()),
    )
    .unwrap();
  }
  writeln!(f, "];\n").unwrap();
}
