///|
fn edit_problem(code : String, message : String) -> Diagnostic {
  diagnostic(code, message, span(0, 0, 1, 1))
}

///|
fn editable(doc : Document) -> Bool {
  !doc.diagnostics.any(d => d.severity == Error && d.code != "INI009")
}

///|
fn encode_quoted(value : String) -> String {
  let out = StringBuilder()
  out.write_string("\"")
  for c in value.iter() {
    match c {
      '\\' => out.write_string("\\\\")
      '"' => out.write_string("\\\"")
      '\n' => out.write_string("\\n")
      '\r' => out.write_string("\\r")
      '\t' => out.write_string("\\t")
      _ => out.write_char(c)
    }
  }
  out.write_string("\"")
  out.to_string()
}

///|
fn encode_value(value : String, previous : String, dialect : Dialect) -> String {
  if dialect.multiline &&
    previous.has_prefix("\"\"\"") &&
    !value.contains("\"\"\"") &&
    !value.has_suffix("\"") {
    return "\"\"\"" + value + "\"\"\""
  }
  if previous.has_prefix("'") &&
    !value.contains("'") &&
    !value.contains("\n") &&
    !value.contains("\r") {
    return "'" + value + "'"
  }
  if value == "" {
    return encode_quoted(value)
  }
  if previous.has_prefix("\"") {
    return encode_quoted(value)
  }
  // Probe the actual selected dialect, not a divergent second set of quoting rules.
  if !value.contains("\n") && !value.contains("\r") {
    let probe = parse("x=" + value, dialect~)
    if !probe.has_errors() &&
      probe.get("", "x") == Some(value) &&
      probe.nodes.length() == 1 {
      return value
    }
  }
  encode_quoted(value)
}

///|
/// Replace one duplicate occurrence (zero-based) without touching any surroundings.
/// Syntax errors block edits; duplicate diagnostics alone do not.
pub fn Document::set_at(
  self : Document,
  section : String,
  key : String,
  occurrence : Int,
  value : String,
) -> Result[Document, Diagnostic] {
  if !editable(self) {
    return Err(edit_problem("EDIT001", "repair syntax errors before editing"))
  }
  let entries = self.entries(section, key)
  if occurrence < 0 || occurrence >= entries.length() {
    return Err(edit_problem("EDIT002", "occurrence does not exist"))
  }
  let node = entries[occurrence]
  if node.value == value {
    return Ok(self)
  }
  let old = text(self.source, node.value_span.start, node.value_span.end)
  let mut token = encode_value(value, old, self.dialect)
  // An empty bare value may share its insertion point with the comment marker.
  // Add a delimiter, keeping every original comment character untouched.
  if node.value_span.start == node.value_span.end &&
    node.value_span.end < self.source.length() &&
    self.dialect.inline_comments &&
    is_comment(self.source[node.value_span.end], self.dialect) {
    token += " "
  }
  let next_source = text(self.source, 0, node.value_span.start) +
    token +
    text(self.source, node.value_span.end, self.source.length())
  let next = parse(next_source, dialect=self.dialect, limits=self.limits)
  let updated = next.entries(section, key)
  if !editable(next) ||
    updated.length() != entries.length() ||
    updated[occurrence].value != value {
    return Err(
      edit_problem("EDIT003", "replacement failed parse-back verification"),
    )
  }
  Ok(next)
}

///|
/// Set the effective existing occurrence; Reject refuses ambiguous duplicates.
pub fn Document::set(
  self : Document,
  section : String,
  key : String,
  value : String,
) -> Result[Document, Diagnostic] {
  let entries = self.entries(section, key)
  if entries.is_empty() {
    return self.insert(section, key, value)
  }
  if entries.length() > 1 && self.dialect.duplicates == Reject {
    return Err(edit_problem("EDIT004", "ambiguous duplicate; use set_at"))
  }
  let index = if self.dialect.duplicates == LastWins {
    entries.length() - 1
  } else {
    0
  }
  self.set_at(section, key, index, value)
}