///| 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
}