///|
#cfg(platform="windows")
fn git_path_spelling(path : String) -> Bool {
  !path.contains("\\") && !path.contains(":")
}

///|
#cfg(not(platform="windows"))
fn git_path_spelling(_path : String) -> Bool {
  true
}

///|
/// Current working-tree rules for Git paths, including deleted files.
struct PathFilter {
  root : String
  options : SelectionOptions
  includes : Array[Pattern]
  custom : Array[Pattern]
  defaults : Array[Pattern]
  inherited : Array[Rule]
  blocked : Bool
  output : String?
  cache : Map[String, Array[Rule]]
}

///|
/// The root is the canonical root returned by `select`.
pub async fn path_filter(
  root : String,
  options : SelectionOptions,
) -> PathFilter {
  if !absolute(root) || optional_kind(root) != Some(Directory) {
    raise SelectionError::InvalidRoot(root)
  }
  let (inherited, blocked) = inherited_rules(root, options)
  let output = match options.output {
    None => None
    Some(path) => Some(normalize(output_identity(path)))
  }
  {
    root,
    options,
    includes: options.include_patterns.map(p => {
      compile_pattern(p, "include", 0, true)
    }),
    custom: options.ignore.map(p => compile_pattern(p, "ignore", 0, true)),
    defaults: if options.use_default_patterns {
      [".git", ".hg", ".svn", "node_modules", "_build", ".DS_Store"].map(p => {
        compile_pattern(p, "default", 0, true)
      })
    } else {
      []
    },
    inherited,
    blocked: blocked is Some(_),
    output,
    cache: Map([]),
  }
}

///|
/// Apply include/ignore, ancestor pruning and no-follow rules without opening a file.
/// Missing paths are valid because Git diffs and history can describe deletions.
pub async fn PathFilter::allows(self : PathFilter, relative : String) -> Bool {
  if self.blocked ||
    relative == "" ||
    absolute(relative) ||
    !git_path_spelling(relative) ||
    relative.contains("\u{0}") {
    return false
  }
  let parts = relative.split("/").map(p => p.to_owned()).collect()
  if parts.any(p => p == "" || p == "." || p == "..") {
    return false
  }
  let mut directory = self.root
  let mut inherited = self.inherited
  let mut partial = ""
  for i in 0.. rules
      None => {
        let rules = local_rules(directory, inherited, self.options)
        self.cache[directory] = rules
        rules
      }
    }
    let part = parts[i]
    partial = if partial == "" { part } else { partial + "/" + part }
    let path = join(directory, part)
    let is_dir = i < parts.length() - 1
    match optional_kind(path) {
      Some(SymLink) => return false
      Some(Directory) if !is_dir => return false
      Some(Regular) if is_dir => return false
      Some(kind) if kind != Regular && kind != Directory => return false
      _ => ()
    }
    if self.defaults.any(p => p.matches(partial, is_dir)) ||
      self.custom.any(p => p.matches(partial, is_dir)) ||
      ignored_by(rules, path, is_dir) is Some(_) {
      return false
    }
    if !is_dir &&
      self.output is Some(output) &&
      normalize(output_identity(path)) == output {
      return false
    }
    directory = path
    inherited = rules
  }
  included(self.includes, relative)
}