// Shared support for embedded-runtime builds on the window-hosted route.
//
// This file contains only the common MoonBit-C, Skia, Android NDK, and response
// file helpers consumed by the Android, iOS, and HarmonyOS window templates.

///|
let default_android_ndk_version : String = "28.2.13676358"

///|
/// Android ABI -> Skia arch.
let android_abi_to_skia_arch : Array[(String, String)] = [
  ("arm64-v8a", "arm64"),
  ("x86_64", "x64"),
  ("riscv64", "riscv64"),
]

///|
/// Android ABI -> LLVM triple.
let android_abi_to_triple : Array[(String, String)] = [
  ("arm64-v8a", "aarch64-linux-android"),
  ("x86_64", "x86_64-linux-android"),
  ("riscv64", "riscv64-linux-android"),
]

///|
/// iOS SDK -> Skia platform.
let ios_sdk_to_skia_platform : Array[(String, String)] = [
  ("iphonesimulator", "iosSim"),
  ("iphoneos", "ios"),
]

///|
/// iOS arch -> Skia arch.
let ios_arch_to_skia_arch : Array[(String, String)] = [
  ("arm64", "arm64"),
  ("x86_64", "x64"),
]

///|
/// Environment variables that must be stripped from a `moon build` spawn so
/// Xcode-injected SDK settings do not leak into a host native build.
let moon_build_env_blocked_keys : Array[String] = [
  "SDKROOT", "SDK_NAME", "PLATFORM_NAME", "EFFECTIVE_PLATFORM_NAME", "ARCHS", "VALID_ARCHS",
  "IPHONEOS_DEPLOYMENT_TARGET", "LLVM_TARGET_TRIPLE_OS_VERSION", "LLVM_TARGET_TRIPLE_SUFFIX",
  "LLVM_TARGET_TRIPLE_VENDOR",
]

///|
priv struct SkiaLink {
  stub_flags : String
  link_flags : String
}

///|
fn lookup_static_map(
  entries : Array[(String, String)],
  key : String,
) -> String? {
  for entry in entries {
    if entry.0 == key {
      return Some(entry.1)
    }
  }
  None
}

///|
fn read_file_bytes_optional(path : String) -> Bytes? {
  Some(@fs.read_file_to_bytes(path)) catch {
    _ => None
  }
}

///|
fn write_file_bytes_result(
  path : String,
  bytes : Bytes,
) -> Result[Unit, String] {
  @fs.write_bytes_to_file(path, bytes) catch {
    _ => return Err("could not write file: " + path)
  }
  Ok(())
}

///|
fn split_flags(flags : String) -> Array[String] {
  let trimmed = flags.trim()
  if trimmed == "" {
    return []
  }
  let result : Array[String] = []
  let current = StringBuilder()
  let mut in_token = false
  for ch in trimmed {
    if ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' {
      if in_token {
        result.push(current.to_string())
        current.reset()
        in_token = false
      }
    } else {
      current.write_char(ch)
      in_token = true
    }
  }
  if in_token {
    result.push(current.to_string())
  }
  result
}

///|
fn first_link_dir(flags : String) -> String {
  for flag in split_flags(flags) {
    if flag.has_prefix("-L") && flag.length() > 2 {
      return flag[2:].to_owned()
    }
  }
  ""
}

///|
fn contains_flag(flags : String, expected : String) -> Bool {
  split_flags(flags).contains(expected)
}

///|
fn android_libcxx_shared_path(ndk_home : String, abi : String) -> String {
  let triple = match lookup_static_map(android_abi_to_triple, abi) {
    Some(value) => value
    None => return ""
  }
  let prebuilt_root = join_path(ndk_home, "toolchains/llvm/prebuilt")
  let hosts = @fs.read_dir(prebuilt_root).to_owned() catch { _ => return "" }
  let sorted_hosts = hosts.copy()
  sorted_hosts.sort_by((a, b) => a.lexical_compare(b))
  for host in sorted_hosts {
    let candidate = join_path(
      join_path(join_path(prebuilt_root, host), "sysroot/usr/lib"),
      triple + "/libc++_shared.so",
    )
    if path_is_file(candidate) {
      return candidate
    }
  }
  ""
}

///|
fn resolve_android_ndk_home(sdk_root : String) -> String {
  let requested = @env.get_env_var("MOUI_ANDROID_NDK_VERSION").unwrap_or(
    default_android_ndk_version,
  )
  let requested_version = match first_numeric_version(requested) {
    Some(value) => value
    None => return ""
  }
  if !numeric_version_at_least(requested_version, [28, 2]) {
    return ""
  }
  let candidates = [
    @env.get_env_var("ANDROID_NDK_HOME").unwrap_or(""),
    @env.get_env_var("ANDROID_NDK_ROOT").unwrap_or(""),
    join_path(sdk_root, "ndk/" + requested),
  ]
  for candidate in candidates {
    if candidate == "" || !valid_android_ndk(candidate) {
      continue
    }
    match ndk_revision(candidate) {
      Some(revision) if numeric_version_at_least(revision, [28, 2]) &&
        numeric_version_has_prefix(revision, requested_version) =>
        return candidate
      _ => ()
    }
  }
  ""
}

///|
fn android_sdk_home() -> String {
  match @env.get_env_var("ANDROID_HOME") {
    Some(value) if value != "" => return value
    _ => ()
  }
  match @env.get_env_var("ANDROID_SDK_ROOT") {
    Some(value) if value != "" => return value
    _ => ()
  }
  let home = @env.get_env_var("HOME").unwrap_or("")
  for
    candidate in [
      join_path(home, "Library/Android/sdk"),
      join_path(home, "Android/Sdk"),
    ] {
    if path_is_dir(candidate) {
      return candidate
    }
  }
  ""
}

///|
fn xcrun_value(args : Array[String]) -> String {
  let argv = ["xcrun", ..args]
  match process_exec_argv(argv, capture=true) {
    Ok(result) if result.exit_code == 0 => result.stdout.trim().to_owned()
    _ => ""
  }
}

///|
fn normalize_moon_build_env(
  base : Array[(String, String)],
) -> Array[(String, String)] {
  let result : Array[(String, String)] = []
  for entry in base {
    let (key, value) = entry
    if moon_build_env_blocked_keys.contains(key) || key == "MOONBIT_NEW_NATIVE" {
      continue
    }
    result.push((key, value))
  }
  result.push(("MOUI_SKIA_DISABLE_PREBUILD_SKIA", "1"))
  result.push(("MOUI_PDFIUM_DISABLE_PREBUILD_PDFIUM", "1"))
  result.push(("MOONBIT_NEW_NATIVE", "0"))
  result
}

///|
fn moon_build_env() -> Array[(String, String)] {
  let result = normalize_moon_build_env(@env.get_env_vars().to_array())
  if detect_host_platform() is Macos {
    let macos_sdk = xcrun_value(["--sdk", "macosx", "--show-sdk-path"])
    if macos_sdk != "" {
      result.push(("SDKROOT", macos_sdk))
      result.push(("SDK_NAME", "macosx"))
      result.push(("PLATFORM_NAME", "macosx"))
    }
    let macos_cc = xcrun_value(["--sdk", "macosx", "--find", "clang"])
    if macos_cc != "" {
      result.push(("CC", macos_cc))
    }
    let macos_cxx = xcrun_value(["--sdk", "macosx", "--find", "clang++"])
    if macos_cxx != "" {
      result.push(("CXX", macos_cxx))
    }
  }
  result
}

///|
fn moonbit_c_path(
  build_dir : String,
  moon_package : String,
  generated_c : String,
) -> String {
  join_path(
    join_path(
      join_path(join_path(build_dir, "moonbit"), "native/debug/build"),
      moon_package,
    ),
    generated_c,
  )
}

///|
fn generate_moonbit_c(
  workspace_root : String,
  moon_package : String,
  generated_c : String,
  build_dir : String,
) -> Result[String, String] {
  let moonbit_target_dir = join_path(build_dir, "moonbit")
  remove_tree(moonbit_target_dir)
  match mkdir_p(moonbit_target_dir) {
    Ok(_) => ()
    Err(message) => return Err(message)
  }
  let argv = [
    "moon", "build", moon_package, "--target", "native", "--target-dir", moonbit_target_dir,
  ]
  log_command("generate_moonbit_c", argv)
  let env = moon_build_env()
  let result = match
    process_exec_argv(argv, cwd=workspace_root, env~, capture=false) {
    Ok(value) => value
    Err(message) => return Err(message)
  }
  let moonbit_c = moonbit_c_path(build_dir, moon_package, generated_c)
  if result.exit_code != 0 && !path_is_file(moonbit_c) {
    return Err(
      "moon build " +
      moon_package +
      " --target native --target-dir " +
      moonbit_target_dir +
      " failed with exit code " +
      result.exit_code.to_string(),
    )
  }
  if result.exit_code != 0 {
    log_warn(
      "moon build returned " +
      result.exit_code.to_string() +
      "; continuing because generated C exists: " +
      moonbit_c,
    )
  } else {
    log_info("moon build finished; resolving Skia link flags")
  }
  Ok(moonbit_c)
}

///|
fn options_fallback_allows_stub_link(
  fallback : Bool,
  stub_flags : String,
  link_flags : String,
) -> Bool {
  if fallback {
    return true
  }
  if stub_flags.contains("MOUI_SKIA_HAS_SKIA") {
    return true
  }
  link_flags.contains("libskia") || link_flags.contains("-lskia")
}

///|
fn skia_resolve_env() -> Array[(String, String)] {
  let base = @env.get_env_vars()
  let result : Array[(String, String)] = []
  for key, value in base {
    if moon_build_env_blocked_keys.contains(key) {
      continue
    }
    if key == "MOUI_SKIA_DISABLE_PREBUILD_SKIA" ||
      key == "MOUI_PDFIUM_DISABLE_PREBUILD_PDFIUM" {
      continue
    }
    result.push((key, value))
  }
  result
}

///|
fn resolve_skia(
  skia_root : String,
  platform : String,
  arch : String,
  link_mode : String,
  fallback : Bool,
  gpu_env : Array[(String, String)],
) -> Result[SkiaLink, String] {
  if fallback {
    return Ok({ stub_flags: "", link_flags: "", })
  }
  let env = skia_resolve_env()
  env.push(("MOUI_SKIA_PLATFORM", platform))
  env.push(("MOUI_SKIA_ARCH", arch))
  env.push(("MOUI_SKIA_LINK_MODE", link_mode))
  for entry in gpu_env {
    env.push(entry)
  }
  let argv = ["node", "build.js"]
  log_command("resolve_skia", argv)
  match process_exec_argv(argv, cwd=skia_root, env~, capture=true) {
    Ok(result) if result.exit_code == 0 => {
      let parsed = @json.parse(result.stdout) catch {
        _ => return Err("moui_skia build.js produced invalid JSON")
      }
      guard parsed is Object(fields) else {
        return Err("moui_skia build.js output is not an object")
      }
      let vars = match fields.get("vars") {
        Some(Object(value)) => value
        _ => return Err("moui_skia build.js output is missing .vars object")
      }
      let stub_flags = match vars.get("MOUI_SKIA_STUB_CC_FLAGS") {
        Some(String(value)) => value
        _ => ""
      }
      let link_flags = match vars.get("MOUI_SKIA_CC_LINK_FLAGS") {
        Some(String(value)) => value
        _ => ""
      }
      if !options_fallback_allows_stub_link(fallback, stub_flags, link_flags) {
        return Err(
          "moui_skia resolved stub-only link flags (no real Skia). " +
          "Unset MOUI_SKIA_DISABLE_PREBUILD_SKIA and ensure the platform Skia release is available under moui_skia/.skia-cache. " +
          "got MOUI_SKIA_CC_LINK_FLAGS=" +
          link_flags,
        )
      }
      log_info(
        "resolved Skia link_mode=" +
        link_mode +
        " platform=" +
        platform +
        " flags_len=" +
        link_flags.length().to_string(),
      )
      Ok({ stub_flags, link_flags, })
    }
    Ok(result) =>
      Err(
        "node build.js failed with exit code " +
        result.exit_code.to_string() +
        (if result.stderr != "" { ": " + result.stderr } else { "" }),
      )
    Err(message) => Err(message)
  }
}

///|
fn copy_android_shared_libs(
  abi : String,
  ndk_home : String,
  skia_link_flags : String,
  jni_libs_dir : String,
) -> Result[Unit, String] {
  let abi_dir = join_path(jni_libs_dir, abi)
  remove_tree(abi_dir)
  match mkdir_p(abi_dir) {
    Ok(_) => ()
    Err(message) => return Err(message)
  }
  let libcxx = android_libcxx_shared_path(ndk_home, abi)
  if libcxx != "" && path_is_file(libcxx) {
    let destination = join_path(abi_dir, "libc++_shared.so")
    match read_file_bytes_optional(libcxx) {
      Some(bytes) => {
        match write_file_bytes_result(destination, bytes) {
          Ok(_) => ()
          Err(message) => return Err(message)
        }
        let size = bytes.length()
        if size < 2_000_000 {
          return Err(
            "Android libc++_shared.so from " +
            libcxx +
            " is only " +
            size.to_string() +
            " bytes; expected a full NDK libc++ (>=2MB). " +
            "Pin ANDROID_NDK_HOME to a complete NDK and wipe artifacts/*/jniLibs before rebuild.",
          )
        }
        log_info(
          "packaged libc++_shared.so from " +
          libcxx +
          " (" +
          size.to_string() +
          " bytes)",
        )
      }
      None =>
        log_warn(
          "libc++_shared.so not readable for abi=" +
          abi +
          " ndkHome=" +
          ndk_home,
        )
    }
  } else {
    log_warn(
      "libc++_shared.so not found for abi=" +
      abi +
      " ndkHome=" +
      ndk_home +
      "; AGP will supply STL from its ndkVersion",
    )
  }
  if contains_flag(skia_link_flags, "-lskia") {
    let skia_lib_dir = first_link_dir(skia_link_flags)
    if skia_lib_dir != "" {
      let skia_so = join_path(skia_lib_dir, "libskia.so")
      if path_is_file(skia_so) {
        match read_file_bytes_optional(skia_so) {
          Some(bytes) =>
            match
              write_file_bytes_result(join_path(abi_dir, "libskia.so"), bytes) {
              Ok(_) => ()
              Err(message) => return Err(message)
            }
          None => ()
        }
      }
    }
  }
  Ok(())
}

///|
fn write_rsp(path : String, flags : String) -> Result[Unit, String] {
  match mkdir_p(parent_dir(path)) {
    Ok(_) => ()
    Err(message) => return Err(message)
  }
  let text = split_flags(flags).join("\n") + "\n"
  match write_file_bytes_result(path, @utf8.encode(text[:], bom=false)) {
    Ok(_) => Ok(())
    Err(message) => Err(message)
  }
}