// Dependency discovery.
//
// A Windows program is not one file: it is an executable and the libraries it
// loads. A bundle that carries only the executable starts and then stops
// with STATUS_DLL_NOT_FOUND, and the list of what is missing is written in
// the import table of the executable. This is where that list is read.
//
// What can be found is what the binary says out loud: the libraries it names
// in its import table. What cannot be found is what it opens at runtime with
// LoadLibrary, and every data file it reads; neither is written down
// anywhere, so only the caller can say what those are.

///|
/// The DLLs that a program needs from the folder it sits in.
///
/// The names come from the import table of the program and from the import
/// tables of the libraries it needs in turn. Only the ones that are actually
/// next to the program are returned: a library that is not there is either
/// part of the system or one the program finds somewhere else, and neither is
/// something a bundle can carry.
///
/// The paths that come back are absolute, and they are the files themselves:
/// the ones next to the program, in the order they were found.
pub async fn discover_dependencies(exe : String) -> Array[String] {
  let out = []
  let seen = []
  let dir = folder_of(norm_sep(exe))
  // The folder is looked through once: the name in an import table is
  // spelled the way the library was built, and the file on the disk may be
  // spelled differently.
  let files = @fs.readdir(dir) catch { _ => [] }
  let queue = [norm_sep(exe)]
  while !queue.is_empty() {
    let current = queue.pop().unwrap()
    if seen.contains(current) {
      continue
    }
    seen.push(current)
    for name in imported_dlls(current) {
      // A name of the system is never a file of the program.
      if is_api_set_dll(name) {
        continue
      }
      if is_known_dll(name) {
        continue
      }
      match find_in(files, name) {
        None => ()
        Some(file) => {
          let path = norm_sep(dir + "/" + file)
          if out.contains(path) {
            continue
          }
          out.push(path)
          // A library needs libraries of its own, and they are looked for in
          // the same folder: that is where Windows looks for them too.
          queue.push(path)
        }
      }
    }
  }
  out
}

///|
/// The folder a path is in, and "." for a path without a folder.
fn folder_of(path : String) -> String {
  let dir = dirname(path)
  if dir == "" {
    "."
  } else {
    dir
  }
}

///|
/// The names of the libraries that the file at `path` imports.
///
/// A file that is not a PE image imports nothing, and neither does a file
/// that cannot be read: there is nothing to find in either.
async fn imported_dlls(path : String) -> Array[String] {
  let data = @fs.read_binary(path) catch { _ => return [] }
  let names = pe_import_dlls(data)
  let delayed = pe_delay_import_dlls(data)
  let out = []
  for n in names {
    out.push(n)
  }
  for n in delayed {
    if !out.contains(n) {
      out.push(n)
    }
  }
  out
}

///|
/// The file in `files` that is called `name`.
///
/// The name in an import table is spelled the way the library was built, and
/// the file on the disk may be spelled differently: Windows does not care
/// about the case. The name of the file on the disk is the one that is
/// returned, because that is the name the file has to keep in the bundle.
fn find_in(files : Array[String], name : String) -> String? {
  for f in files {
    if f == name {
      return Some(f)
    }
  }
  for f in files {
    if same_name(f, name) {
      return Some(f)
    }
  }
  None
}

///|
/// Is `name` the name of an API set?
///
/// An API set is a name the system gives to a group of functions, and the
/// library that carries it is a virtual one: there is no such file on the
/// disk, and the system resolves the name to a real library when the
/// program is started. A bundle that carried one would carry nothing.
pub fn is_api_set_dll(name : String) -> Bool {
  starts_with_ignoring_case(name, "api-ms-") ||
  starts_with_ignoring_case(name, "ext-ms-")
}

///|
/// Is `name` a library the system always loads from its own copy?
///
/// The system keeps a list of these, and it loads them from itself whatever
/// the program carries: the file next to the program is never used, so
/// carrying it would only make the bundle bigger.
pub fn is_known_dll(name : String) -> Bool {
  for k in known_dlls() {
    if same_name(k, name) {
      return true
    }
  }
  false
}

///|
/// The libraries the system loads from its own copy.
///
/// This is the list of Windows 10 and 11. It is a list and not a lookup in
/// the registry, because the registry of one machine is not the registry of
/// the machine the bundle is made for, and a bundle that leaves out a library
/// because this machine did not know it would fail on the other one.
fn known_dlls() -> Array[String] {
  [
    "advapi32.dll", "bcrypt.dll", "bcryptprimitives.dll", "bootvid.dll", "calsrv.dll",
    "cfgmgr32.dll", "clbcatq.dll", "combase.dll", "comdlg32.dll", "compositebus.dll",
    "credui.dll", "crypt32.dll", "cryptsp.dll", "d2d1.dll", "d3d10warp.dll", "d3d11.dll",
    "d3d12.dll", "d3d9.dll", "d3dim.dll", "dcomp.dll", "ddraw.dll", "dhcpapi.dll",
    "dhcpcsvc.dll", "dnsapi.dll", "dsound.dll", "dwrite.dll", "dwritecore.dll", "dxgi.dll",
    "efslsrvapi.dll", "embeddedprovider.dll", "eventvwr.dll", "faults.dll", "fwpuclnt.dll",
    "gdi32.dll", "gdi32full.dll", "gdiplus.dll", "gpapi.dll", "hiddenwindow.dll",
    "hostname.dll", "imm32.dll", "imagehlp.dll", "imeui.dll", "inf.dll", "infascii.dll",
    "integrity.dll", "iphlpapi.dll", "itss.dll", "kernel32.dll", "kernelbase.dll",
    "ksecdd.dll", "licui.dll", "locale.dll", "manpath.dll", "mfplat.dll", "mgdmts.dll",
    "midl.dll", "mpr.dll", "msvcp140.dll", "ncrypt.dll", "nsi.dll", "normaliz.dll",
    "ole32.dll", "oleacdui.dll", "oleaut32.dll", "oledlg.dll", "oledir.dll", "olepro32.dll",
    "olesvr32.dll", "oletypes.dll", "powrprof.dll", "profapi.dll", "propsys.dll",
    "psapi.dll", "pstorec.dll", "psxml.dll", "rasapi32.dll", "rpcrt4.dll", "sechost.dll",
    "sensapi.dll", "shcore.dll", "shell32.dll", "shfolder.dll", "shlwapi.dll", "slwapi.dll",
    "snxhk.dll", "strmdll.dll", "ucrtbase.dll", "user32.dll", "userenv.dll", "wdmsec.dll",
    "winspool.drv", "wldap32.dll", "wtsapi32.dll", "wtvfw32.dll", "xaml.dll", "xmllite.dll",
  ]
}

///|
/// Are the two names the same, whatever the case of the letters?
///
/// Windows does not care about the case of a file name, and the name in an
/// import table is spelled the way the library was built.
fn same_name(a : String, b : String) -> Bool {
  let x = string_to_utf8_bytes(a)
  let y = string_to_utf8_bytes(b)
  if x.length() != y.length() {
    return false
  }
  let mut i = 0
  while i < x.length() {
    if lower_byte(x.at(i)) != lower_byte(y.at(i)) {
      return false
    }
    i = i + 1
  }
  true
}

///|
/// Does `name` start with `prefix`, whatever the case of the letters?
fn starts_with_ignoring_case(name : String, prefix : String) -> Bool {
  let n = string_to_utf8_bytes(name)
  let p = string_to_utf8_bytes(prefix)
  if n.length() < p.length() {
    return false
  }
  let mut i = 0
  while i < p.length() {
    if lower_byte(n.at(i)) != p.at(i) {
      return false
    }
    i = i + 1
  }
  true
}

///|
/// The byte in lower case, for the letters of the Latin alphabet.
fn lower_byte(b : Byte) -> Byte {
  if b >= b'A' && b <= b'Z' {
    (b.to_int() + 32).to_byte()
  } else {
    b
  }
}