///|
fn relative_to_root(root : String, full : String) -> String {
  let normalized_root = root.replace_all(old="\\", new="/")
  let normalized_full = full.replace_all(old="\\", new="/")
  if normalized_full == normalized_root {
    ""
  } else if normalized_full.has_prefix(normalized_root + "/") {
    normalized_full[normalized_root.length() + 1:].to_owned()
  } else {
    normalize_relative_path(normalized_full)
  }
}

///|
fn scanner_should_exclude(relative : String, options : ScanOptions) -> Bool {
  path_depth(relative) > options.max_depth ||
  (!options.include_hidden && is_hidden_path(relative)) ||
  matches_any_pattern(relative, options.ignore_patterns)
}

///|
fn scanner_kind(kind : @fs.FileKind) -> EntryKind {
  match kind {
    Regular => EntryKind::Regular
    Directory => EntryKind::Directory
    SymLink => EntryKind::Symlink
    _ => EntryKind::Other
  }
}

///|
/// Scan a real directory using MoonBit's native asynchronous filesystem API.
/// Symbolic links are recorded but not followed unless the caller explicitly opts in.
pub async fn scan_directory(
  root : String,
  created_at : Int64,
  options? : ScanOptions = ScanOptions::default(),
) -> Snapshot {
  let canonical = @fs.realpath(root)
  let entries : Array[Entry] = []
  let errors : Array[ScanError] = []
  let mut stopped = false
  @fs.walk(
    canonical,
    max_concurrency=options.max_concurrency,
    allow_failure=true,
    exclude=fn(path) {
      let relative = relative_to_root(canonical, path)
      relative != "" && scanner_should_exclude(relative, options)
    },
    async fn(directory, names) {
      if stopped {
        return
      }
      for name in names {
        if entries.length() >= options.max_entries {
          stopped = true
          errors.push(ScanError::{
            path: relative_to_root(canonical, directory),
            operation: "scan",
            message: "The configured maximum entry count was reached.",
          })
          return
        }
        let full = directory + "/" + name
        let relative = relative_to_root(canonical, full)
        if scanner_should_exclude(relative, options) {
          continue
        }
        try {
          let kind = @fs.kind(full, follow_symlink=options.follow_symlinks)
          let entry_kind = scanner_kind(kind)
          let (seconds, _) = @fs.mtime(
            full,
            follow_symlink=options.follow_symlinks,
          )
          let size = if entry_kind == EntryKind::Regular {
            let file = @fs.open(full, mode=ReadOnly)
            defer file.close()
            file.size()
          } else {
            0L
          }
          entries.push(Entry::new(relative, entry_kind, size, seconds))
        } catch {
          error =>
            errors.push(ScanError::{
              path: relative,
              operation: "metadata",
              message: @debug.to_string(error),
            })
        }
      }
    },
  )
  entries.sort_by(fn(a, b) {
    if a.path < b.path {
      -1
    } else if a.path > b.path {
      1
    } else {
      0
    }
  })
  Snapshot::new(canonical, created_at, entries, errors~)
}