///|
// One structural exclusion, rendered either as the legacy diagnostic or as a
// safe query decision. Keep rule text and absolute rule paths inside this type.
priv enum Exclusion {
  Fixed(String)
  Pattern(String, Int32, Bool)
  RuleFile(Rule, Bool)
}

///|
fn ignored_reason(is_dir : Bool) -> String {
  if is_dir {
    "IgnoredDirectory"
  } else {
    "Ignored"
  }
}

///|
fn Exclusion::reason(self : Exclusion) -> String {
  match self {
    Fixed(reason) => reason
    Pattern(kind, _, is_dir) => "\{ignored_reason(is_dir)}:\{kind}"
    RuleFile(rule, is_dir) =>
      "\{ignored_reason(is_dir)}:\{rule.source}:\{rule.line}"
  }
}

///|
fn Exclusion::decision(
  self : Exclusion,
  root : String,
  path : String,
) -> PathDecision {
  let (reason, origin) = match self {
    Fixed(reason) => (reason, None)
    Pattern(kind, index, is_dir) =>
      (
        ignored_reason(is_dir),
        Some({ kind, source: None, line: None, index: Some(index), }),
      )
    RuleFile(rule, is_dir) => {
      let kind = match basename(rule.source) {
        ".gitignore" => "gitignore"
        ".ignore" => "dot-ignore"
        ".repomixignore" => "repomixignore"
        _ => "git-info-exclude"
      }
      (
        ignored_reason(is_dir),
        Some({
          kind,
          source: within(rule.source, root),
          line: Some(rule.line),
          index: None,
        }),
      )
    }
  }
  { path, eligible: false, reason, origin, }
}

///|
fn candidate_decision(path : String) -> PathDecision {
  { path, eligible: true, reason: "Candidate", origin: None, }
}

///|
fn type_exclusion(kind : @fs.FileKind?) -> Exclusion? {
  match kind {
    Some(SymLink) => Some(Fixed("Symlink"))
    Some(kind) if kind != Regular && kind != Directory =>
      Some(Fixed("UnsupportedFileType"))
    _ => None
  }
}

///|
// The walker and exact/Git paths share the same ordered exclusion layers.
// The caller checks type, spelling and component position before this function.
fn PathFilter::exclusion(
  self : PathFilter,
  path : String,
  output_path : String,
  parts : ArrayView[String],
  is_dir : Bool,
  rules : Array[Rule],
) -> Exclusion? {
  if self.output == Some(output_path) {
    return Some(Fixed("OutputFile"))
  }
  for index, pattern in self.defaults {
    if pattern.matches_parts(parts, is_dir) {
      return Some(Pattern("default", index, is_dir))
    }
  }
  for index, pattern in self.custom {
    if pattern.matches_parts(parts, is_dir) {
      return Some(Pattern("custom", index, is_dir))
    }
  }
  ignored_by(rules, path, is_dir).map(rule => RuleFile(rule, is_dir))
}

///|
fn PathFilter::include_exclusion(
  self : PathFilter,
  parts : ArrayView[String],
) -> Exclusion? {
  if included_parts(self.includes, parts) {
    None
  } else {
    Some(Fixed("NotIncluded"))
  }
}