///|
/// A simple editable operation over a parsed document.
pub enum DocumentEdit {
  SetValue(String, String)
  RemoveValue(String)
  RenameValue(String, String)
} derive(@debug.Debug, Eq)

///|
pub fn set_value(path : String, value : String) -> DocumentEdit {
  SetValue(path, value)
}

///|
pub fn remove_value(path : String) -> DocumentEdit {
  RemoveValue(path)
}

///|
pub fn rename_value(old_path : String, new_path : String) -> DocumentEdit {
  RenameValue(old_path, new_path)
}

///|
/// Returns entries from one section.
pub fn ConfigDocument::section_entries(
  self : ConfigDocument,
  section : String,
) -> Array[ConfigEntry] {
  let result : Array[ConfigEntry] = []
  for entry in self.entries {
    if entry.section == section {
      result.push(entry)
    }
  }
  result
}

///|
/// Returns all section names in declaration order.
pub fn ConfigDocument::sections(self : ConfigDocument) -> Array[String] {
  let result : Array[String] = []
  for entry in self.entries {
    if entry.section != "" && !doc_contains(result, entry.section) {
      result.push(entry.section)
    }
  }
  result
}

///|
/// Finds the last entry matching a path.
pub fn ConfigDocument::get_path(
  self : ConfigDocument,
  path : String,
) -> String? {
  let mut found : String? = None
  for entry in self.entries {
    if entry.path() == path {
      found = Some(entry.value)
    }
  }
  found
}

///|
/// Returns all duplicate paths in a document.
pub fn ConfigDocument::duplicate_paths(self : ConfigDocument) -> Array[String] {
  let seen : Array[String] = []
  let duplicates : Array[String] = []
  for entry in self.entries {
    let path = entry.path()
    if doc_contains(seen, path) && !doc_contains(duplicates, path) {
      duplicates.push(path)
    }
    seen.push(path)
  }
  duplicates
}

///|
/// Creates a new document with an edited value.
pub fn ConfigDocument::with_value(
  self : ConfigDocument,
  path : String,
  value : String,
) -> ConfigDocument {
  let entries : Array[ConfigEntry] = []
  let mut replaced = false
  for entry in self.entries {
    if entry.path() == path {
      entries.push({
        section: entry.section,
        key: entry.key,
        value,
        source: entry.source,
        span: entry.span,
        comments: entry.comments,
      })
      replaced = true
    } else {
      entries.push(entry)
    }
  }
  if !replaced {
    let parts = split_path(path)
    entries.push({
      section: parts.0,
      key: parts.1,
      value,
      source: self.name,
      span: span(self.name, self.entries.length() + 1, 1),
      comments: [],
    })
  }
  { name: self.name, entries, diagnostics: self.diagnostics }
}

///|
/// Creates a new document without a path.
pub fn ConfigDocument::without_path(
  self : ConfigDocument,
  path : String,
) -> ConfigDocument {
  let entries : Array[ConfigEntry] = []
  for entry in self.entries {
    if entry.path() != path {
      entries.push(entry)
    }
  }
  { name: self.name, entries, diagnostics: self.diagnostics }
}

///|
/// Renames an entry path while preserving value and source metadata.
pub fn ConfigDocument::rename_path(
  self : ConfigDocument,
  old_path : String,
  new_path : String,
) -> ConfigDocument {
  let entries : Array[ConfigEntry] = []
  let parts = split_path(new_path)
  for entry in self.entries {
    if entry.path() == old_path {
      entries.push({
        section: parts.0,
        key: parts.1,
        value: entry.value,
        source: entry.source,
        span: entry.span,
        comments: entry.comments,
      })
    } else {
      entries.push(entry)
    }
  }
  { name: self.name, entries, diagnostics: self.diagnostics }
}

///|
/// Applies a sequence of document edits.
pub fn ConfigDocument::apply_edits(
  self : ConfigDocument,
  edits : Array[DocumentEdit],
) -> ConfigDocument {
  let mut doc = self
  for edit in edits {
    match edit {
      SetValue(path, value) => doc = doc.with_value(path, value)
      RemoveValue(path) => doc = doc.without_path(path)
      RenameValue(old_path, new_path) =>
        doc = doc.rename_path(old_path, new_path)
    }
  }
  doc
}

///|
fn split_path(path : String) -> (String, String) {
  let parts = path.split(".").map(part => part.to_owned()).collect()
  if parts.length() <= 1 {
    ("", path)
  } else {
    let key = parts[parts.length() - 1]
    let section_parts = parts[0:parts.length() - 1]
    (section_parts.join("."), key)
  }
}

///|
fn doc_contains(values : Array[String], target : String) -> Bool {
  for value in values {
    if value == target {
      return true
    }
  }
  false
}

///|
/// Extracts a document containing only keys below a section.
pub fn ConfigDocument::subdocument(
  self : ConfigDocument,
  section : String,
) -> ConfigDocument {
  let entries : Array[ConfigEntry] = []
  for entry in self.entries {
    if entry.section == section || entry.section.has_prefix(section + ".") {
      entries.push(entry)
    }
  }
  { name: self.name + "#" + section, entries, diagnostics: self.diagnostics }
}

///|
/// Renders a document directly as INI, preserving the current entry order.
pub fn ConfigDocument::render_document_ini(self : ConfigDocument) -> String {
  let builder = StringBuilder()
  let mut current_section = ""
  for entry in self.entries {
    if entry.section != current_section {
      if builder.to_string() != "" {
        builder.write_string("\n")
      }
      current_section = entry.section
      if current_section != "" {
        builder.write_string("[" + current_section + "]\n")
      }
    }
    for comment in entry.comments {
      builder.write_string("; " + comment + "\n")
    }
    builder.write_string(entry.key + " = " + entry.value + "\n")
  }
  builder.to_string()
}

///|
/// Creates an INI patch from edits without mutating the original document.
pub fn render_edit_patch(
  document : ConfigDocument,
  edits : Array[DocumentEdit],
) -> String {
  let edited = document.apply_edits(edits)
  let diff = diff_views(
    merge_layers([layer("before", 0, document)]),
    merge_layers([layer("after", 0, edited)]),
  )
  render_diff_markdown(diff)
}

///|
/// Returns entries whose key names match a prefix.
pub fn ConfigDocument::find_key_prefix(
  self : ConfigDocument,
  prefix : String,
) -> Array[ConfigEntry] {
  let entries : Array[ConfigEntry] = []
  for entry in self.entries {
    if entry.key.has_prefix(prefix) || entry.path().has_prefix(prefix) {
      entries.push(entry)
    }
  }
  entries
}

///|
/// Returns a warning for keys that do not follow lowercase dot/underscore style.
pub fn ConfigDocument::key_style_diagnostics(
  self : ConfigDocument,
) -> Array[Diagnostic] {
  let diagnostics : Array[Diagnostic] = []
  for entry in self.entries {
    let path = entry.path()
    if !is_style_key(path) {
      diagnostics.push(
        warning(
          "key-style",
          "key '" +
          path +
          "' should use lowercase letters, digits, dots, dashes, or underscores",
          span=Some(entry.span),
        ),
      )
    }
  }
  diagnostics
}

///|
fn is_style_key(path : String) -> Bool {
  let mut index = 0
  while index < path.length() {
    let ch = path[index]
    let ok = (ch >= 'a' && ch <= 'z') ||
      (ch >= '0' && ch <= '9') ||
      ch == '.' ||
      ch == '_' ||
      ch == '-'
    if !ok {
      return false
    }
    index = index + 1
  }
  true
}