///|
/// A parsed Make-style dependency file produced by compilers such as GCC.
pub(all) struct Depfile {
  target : String
  dependencies : Array[String]
} derive(Debug, Eq)

///|
fn depfile_unescape(text : String) -> String {
  let mut result = ""
  let mut escaped = false
  for ch in text {
    if escaped {
      result += "\{ch}"
      escaped = false
    } else if ch == '\\' {
      escaped = true
    } else {
      result += "\{ch}"
    }
  }
  if escaped {
    result += "\\"
  }
  result
}

///|
fn depfile_join_continuations(text : String) -> String {
  let mut result = ""
  let mut cursor = 0
  while cursor < text.length() {
    let ch = get_char_at(text, cursor)
    if ch == '\\' && cursor + 1 < text.length() {
      let next = get_char_at(text, cursor + 1)
      if next == '\n' {
        result += " "
        cursor += 2
        continue
      }
      if next == '\r' &&
        cursor + 2 < text.length() &&
        get_char_at(text, cursor + 2) == '\n' {
        result += " "
        cursor += 3
        continue
      }
    }
    result += "\{ch}"
    cursor += 1
  }
  result
}

///|
fn depfile_separator(text : String) -> Int? {
  let mut cursor = 0
  let mut escaped = false
  while cursor < text.length() {
    let ch = get_char_at(text, cursor)
    if escaped {
      escaped = false
    } else if ch == '\\' {
      escaped = true
    } else if ch == ':' {
      return Some(cursor)
    }
    cursor += 1
  }
  None
}

///|
fn depfile_words(text : String) -> Array[String] {
  let words : Array[String] = []
  let mut current = ""
  let mut escaped = false
  for ch in text {
    if escaped {
      current += "\{ch}"
      escaped = false
    } else if ch == '\\' {
      escaped = true
    } else if ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' {
      if current != "" {
        words.push(current)
        current = ""
      }
    } else {
      current += "\{ch}"
    }
  }
  if escaped {
    current += "\\"
  }
  if current != "" {
    words.push(current)
  }
  words
}

///|
/// Parse one or more Make-style depfile rules. Multiple target rules are
/// rejected so callers cannot silently apply the wrong dependency set.
pub fn parse_depfile(text : String) -> Result[Depfile, String] {
  let flattened = depfile_join_continuations(text)
  let separator = match depfile_separator(flattened) {
    Some(value) => value
    None => return Err("depfile is missing ':' separator")
  }
  let raw_target = flattened[:separator].to_owned().trim().to_owned()
  if raw_target == "" {
    return Err("depfile target is empty")
  }
  let targets = depfile_words(raw_target)
  if targets.length() != 1 {
    return Err("depfile must contain exactly one target")
  }
  let raw_dependencies = flattened[separator + 1:].to_owned()
  let dependencies : Array[String] = []
  for word in depfile_words(raw_dependencies) {
    let dependency = depfile_unescape(word).trim().to_owned()
    if dependency != "" && !dependencies.contains(dependency) {
      dependencies.push(dependency)
    }
  }
  Ok({ target: depfile_unescape(targets[0]), dependencies })
}

///|
/// Merge a depfile into the build edge that produces its target.
pub fn Manifest::apply_depfile(
  self : Manifest,
  depfile : Depfile,
) -> Result[Manifest, String] {
  let mut found = false
  let updated_builds : Array[BuildEdge] = []
  for edge in self.builds {
    if edge.outputs.contains(depfile.target) {
      found = true
      let inputs = edge.inputs.copy()
      for dependency in depfile.dependencies {
        if !inputs.contains(dependency) {
          inputs.push(dependency)
        }
      }
      updated_builds.push({ ..edge, inputs, })
    } else {
      updated_builds.push(edge)
    }
  }
  if !found {
    Err("depfile target is not produced by the manifest: " + depfile.target)
  } else {
    Ok({ ..self, builds: updated_builds })
  }
}