///|
fn make_entry(
  root : String,
  full_path : String,
  basename : String,
  follow_symlink : Bool,
  include_stats : Bool,
) -> EntryInfo raise ReaddirpError {
  let stats = stat_path(full_path, follow_symlink~)
  {
    path: relative_path(root, full_path),
    full_path,
    basename,
    kind: file_kind_from_mode(stats.mode),
    stats: if include_stats {
      Some(stats)
    } else {
      None
    },
  }
}

///|
fn should_emit(entry : EntryInfo, type_ : EntryType) -> Bool {
  match entry.kind {
    File => wants_file(type_)
    Directory => wants_directory(type_)
    Symlink | Other => wants_other(type_)
  }
}

///|
fn add_warning(
  warnings : Array[ReaddirpWarning],
  path : String,
  message : String,
) -> Unit {
  warnings.push(
    warning_for(path, warning_kind_from_errno(last_errno()), message),
  )
}

///|
fn add_recursive_symlink_warning(
  warnings : Array[ReaddirpWarning],
  path : String,
  target : String,
) -> Unit {
  warnings.push(
    warning_for(path, RecursiveSymlink, "recursive symlink skipped: " + target),
  )
}

///|
fn dir_is_symlink(path : String) -> Bool raise ReaddirpError {
  file_kind_from_mode(stat_path(path, follow_symlink=false).mode) == Symlink
}

///|
fn child_ancestors(
  ancestors : Array[String],
  child_real_path : String,
) -> Array[String] {
  let next = ancestors.copy()
  next.push(child_real_path)
  next
}

///|
fn walk_dir(
  root : String,
  dir : String,
  ancestors : Array[String],
  current_depth : Int,
  options : Options,
  entries : Array[EntryInfo],
  warnings : Array[ReaddirpWarning],
) -> Unit raise ReaddirpError {
  let names = read_dir_all(dir)
  for name in names {
    let full_path = join_path(dir, name)
    try {
      let entry = make_entry(
        root,
        full_path,
        name,
        !options.lstat,
        options.always_stat,
      )
      if entry.kind == Directory {
        if (options.directory_filter)(entry) {
          if should_emit(entry, options.type_) {
            entries.push(entry)
          }
          if current_depth < options.depth {
            let real_child = real_path(full_path)
            if dir_is_symlink(full_path) && ancestors.contains(real_child) {
              add_recursive_symlink_warning(warnings, full_path, real_child)
            } else {
              walk_dir(
                root,
                full_path,
                child_ancestors(ancestors, real_child),
                current_depth + 1,
                options,
                entries,
                warnings,
              ) catch {
                ReaddirpError(message) =>
                  add_warning(warnings, full_path, message)
              }
            }
          }
        }
      } else if (options.file_filter)(entry) &&
        should_emit(entry, options.type_) {
        entries.push(entry)
      }
    } catch {
      ReaddirpError(message) => add_warning(warnings, full_path, message)
    }
  }
}

///|
pub fn readdirp_report(
  root : String,
  options? : Options = Options::default(),
) -> ReaddirpReport raise ReaddirpError {
  guard root != "" else { raise ReaddirpError("root argument is required") }
  guard options.depth >= 0 else {
    raise ReaddirpError("depth must be non-negative")
  }
  let normalized_root = normalize_path(root)
  let absolute_root = real_path(normalized_root)
  let root_stats = stat_path(absolute_root, follow_symlink=true)
  guard file_kind_from_mode(root_stats.mode) == Directory else {
    raise ReaddirpError("root must be a directory")
  }
  let entries = []
  let warnings = []
  walk_dir(
    absolute_root,
    absolute_root,
    [absolute_root],
    0,
    options,
    entries,
    warnings,
  )
  { entries, warnings }
}

///|
pub fn readdirp(
  root : String,
  options? : Options = Options::default(),
) -> Array[EntryInfo] raise ReaddirpError {
  readdirp_report(root, options~).entries
}