///|
fn replace_stem(word : String, stem : String) -> String {
  match word.split_once("%") {
    Some((before, after)) => before.to_owned() + stem + after.to_owned()
    None => word
  }
}

///|
fn replace_stems(text : String, stem : String) -> String {
  let result = StringBuilder()
  let mut replaced = false
  for char in text {
    if char.is_whitespace() {
      replaced = false
      result.write_char(char)
    } else if char == '%' && !replaced {
      result.write_string(stem)
      replaced = true
    } else {
      result.write_char(char)
    }
  }
  result.to_string()
}

///|
priv struct Prerequisites {
  text : String
  secondary : Bool
  mut has_recipe : Bool
  stem : String?
}

///|
fn distinct_words(words : Array[String]) -> Array[String] {
  let seen = Set([])
  words.filter(word => {
    if seen.contains(word) {
      false
    } else {
      seen.add(word)
      true
    }
  })
}

///|
fn automatic_variables(
  target : String,
  stem : String?,
  dependencies : Array[String],
  newer_dependencies~ : Array[String] = [],
) -> Map[String, String] {
  let automatic : Map[String, String] = {
    "@": target,
    "<": dependencies.get(0).unwrap_or(""),
    "^": distinct_words(dependencies).join(" "),
    "+": dependencies.join(" "),
    "*": stem.unwrap_or(""),
    "?": distinct_words(newer_dependencies).join(" "),
    "%": "",
  }
  for name in ["@", "<", "^", "+", "*", "?"] {
    let words = split_words(automatic[name])
    automatic[name + "D"] = words
      .map(word => @path.Path(word).dirname().to_string())
      .join(" ")
    automatic[name + "F"] = words
      .map(word => @path.Path(word).basename().to_owned())
      .join(" ")
  }
  automatic
}

///|
async fn resolve_prerequisites(
  rule : Rule,
  database : Database,
) -> Array[String] {
  let groups = rule.groups
  // GNU make 4.4.1 prepends the recipe-bearing declaration to its dependency
  // chain. Concrete prerequisites are visible even before deferred entries.
  let expanded = groups.map(group => {
    if group.secondary {
      None
    } else {
      Some(split_words(group.text))
    }
  })
  for index, group in groups {
    if !group.secondary {
      continue
    }
    let known : Array[String] = []
    for words in expanded {
      if words is Some(words) {
        known.append(words)
      }
    }
    let stem = match group.stem {
      Some(_) => group.stem
      None => rule.stem
    }
    expanded[index] = Some(
      split_words(
        expand(
          group.text,
          database.variables,
          automatic_variables(rule.target, stem, known),
          0,
          cwd=database.cwd,
          exported=database.exported,
          state=database.expansion(),
        ),
      ),
    )
  }
  let result : Array[String] = []
  for words in expanded {
    if words is Some(words) {
      result.append(words)
    }
  }
  validate_rule_words([rule.target], result)
  result
}