///|
priv struct SourceSpan {
  path : Array[Int]
  key_start : Int
  key_end : Int
  value_start : Int
  value_end : Int
  entry_end : Int
}

///|
/// A parsed source with scalar-index spans. Edit returns a new, reparsed source;
/// all characters outside the selected replacement range remain unchanged.
struct SourceDocument {
  source : String
  doc : Document
  spans : Array[SourceSpan]
  span_index : Map[String, SourceSpan]
  options : ParseOptions
  chars : Array[Char]
  linebreak : String
  unit : String
}

///|
pub fn parse_preserving(
  input : String,
  options? : ParseOptions = ParseOptions::default(),
) -> SourceDocument raise {
  if options.conditions is Some(_) {
    raise InvalidEdit(
      "Source documents retain all conditions; call evaluate for a filtered view",
    )
  }
  let (doc, _, spans) = parse_with_spans(input, options, true, true)
  let span_index : Map[String, SourceSpan] = Map([])
  for span in spans {
    span_index[index_key(span.path)] = span
  }
  let result = SourceDocument::{
    source: input,
    doc,
    spans,
    span_index,
    options,
    chars: input.to_array(),
    linebreak: if input.contains("\r\n") {
      "\r\n"
    } else if input.contains("\r") {
      "\r"
    } else {
      "\n"
    },
    unit: "  ",
  }
  { ..result, unit: result.detect_indent_unit(), }
}

///|
pub fn SourceDocument::text(self : SourceDocument) -> String {
  self.source
}

///|
pub fn SourceDocument::document(self : SourceDocument) -> Document {
  self.doc
}

///|
/// Evaluate a source without changing its raw view. Root file directives still
/// require load_resolved with an explicitly supplied loader.
pub fn SourceDocument::evaluate(
  self : SourceDocument,
  symbols : Array[String],
) -> Document raise ParseError {
  parse(self.source, options={ ..self.options, conditions: Some(symbols), })
}

///|
fn SourceDocument::span(
  self : SourceDocument,
  path : Array[Int],
) -> SourceSpan raise EditError {
  match self.span_index.get(index_key(path)) {
    Some(span) => span
    None => raise InvalidEdit("Source span was not found")
  }
}

///|
/// Supports the same occurrence-aware edits as Document::edit. Comments outside
/// replaced values remain intact; removing an entry leaves surrounding comments.
/// Newly inserted or replaced structures use canonical escaped KV1 formatting.
pub fn SourceDocument::edit(
  self : SourceDocument,
  path : Array[Selector],
  operation : Edit,
) -> SourceDocument raise {
  // Validate the operation before touching source and determine the exact indices.
  let expected = self.doc.edit(path, operation)
  let indices : Array[Int] = []
  let mut selected = Value::Object(self.doc.entries)
  for selector in path {
    guard selected is Object(entries) else {
      raise InvalidEdit("Path crosses text")
    }
    let mut seen = 0
    for i in 0..
      if indices.is_empty() {
        self.render_entries(builder, expected.entries, "", true)
      } else {
        let span = self.span(indices)
        start = span.value_start
        end = span.value_end
        match value {
          Text(s) =>
            self.render_string(builder, s, chars[span.value_start] == '"')
          Object(children) => {
            let indent = self.line_indent(span.key_start)
            builder.write_string("{" + self.newline())
            self.render_entries(
              builder,
              children,
              indent + self.indent_unit(),
              true,
            )
            builder.write_string(indent)
            builder.write_string("}")
          }
        }
      }
    Rename(key) => {
      let span = self.span(indices)
      start = span.key_start
      end = span.key_end
      self.render_string(builder, key, chars[span.key_start] == '"')
    }
    Remove => {
      let span = self.span(indices)
      start = span.key_start
      end = span.entry_end
    }
    Insert(index, entry) => {
      guard selected is Object(entries) else {
        raise InvalidEdit("Insertion target is not an object")
      }
      start = if index < entries.length() {
        let child = indices.map(i => i)
        child.push(index)
        self.span(child).key_start
      } else if indices.is_empty() {
        chars.length()
      } else {
        self.span(indices).value_end - 1
      }
      end = start
      let indent = if index < entries.length() {
        let child = [..indices, index]
        self.line_indent(self.span(child).key_start)
      } else if indices.is_empty() {
        ""
      } else {
        self.line_indent(self.span(indices).key_start) + self.indent_unit()
      }
      // Insertion starts on its own line; existing trivia stays attached to its
      // original entry. Only the newly written text adopts surrounding style.
      builder.write_string(self.newline())
      let quoted = if !entries.is_empty() {
        let neighbor = [
          ..indices,
          if index < entries.length() {
            index
          } else {
            entries.length() - 1
          },
        ]
        chars[self.span(neighbor).key_start] == '"'
      } else {
        true
      }
      self.render_entries(builder, [entry], indent, quoted)
      if index < entries.length() {
        builder.write_string(indent)
      }
    }
  }
  let source = String::from_array(chars[:start].to_owned()) +
    builder.to_string() +
    String::from_array(chars[end:].to_owned())
  let result = parse_preserving(source, options=self.options)
  if result.doc != expected {
    raise InvalidEdit("Source edit could not preserve requested semantics")
  }
  result
}

///|
fn SourceDocument::newline(self : SourceDocument) -> String {
  self.linebreak
}

///|
fn SourceDocument::line_indent(self : SourceDocument, offset : Int) -> String {
  let chars = self.chars
  let mut start = offset
  while start > 0 && chars[start - 1] != '\n' && chars[start - 1] != '\r' {
    start -= 1
  }
  let prefix = chars[start:offset]
  if prefix.all(c => c == ' ' || c == '\t') {
    String::from_array(prefix.to_owned())
  } else {
    ""
  }
}

///|
fn SourceDocument::indent_unit(self : SourceDocument) -> String {
  self.unit
}

///|
fn SourceDocument::detect_indent_unit(self : SourceDocument) -> String {
  for span in self.spans {
    if span.path.length() > 1 {
      let indent = self.line_indent(span.key_start)
      let parent = self.span(span.path[:span.path.length() - 1].to_owned()) catch {
        _ => continue
      }
      let parent_indent = self.line_indent(parent.key_start)
      if indent.has_prefix(parent_indent) &&
        indent.length() > parent_indent.length() {
        return indent[parent_indent.length():].to_owned()
      }
    }
  }
  "  "
}

///|
fn SourceDocument::render_string(
  self : SourceDocument,
  builder : StringBuilder,
  text : String,
  quoted : Bool,
) -> Unit raise WriteError {
  if text.contains("\u0000") {
    raise InvalidDocument("NUL cannot be written")
  }
  let bare = !text.is_empty() &&
    !text.has_prefix("//") &&
    !text.has_prefix("[") &&
    text.all(c => !is_space(c) && c != '{' && c != '}' && c != '"' && c != '=')
  if !quoted && bare {
    builder.write_string(text)
    return
  }
  if self.options.escape_sequences {
    write_quoted(builder, text)
  } else {
    if text.contains("\"") {
      raise InvalidDocument("A quote cannot be represented in literal mode")
    }
    builder.write_string("\"" + text + "\"")
  }
}

///|
fn SourceDocument::render_entries(
  self : SourceDocument,
  builder : StringBuilder,
  entries : Array[Entry],
  indent : String,
  quoted : Bool,
  depth? : Int = 0,
) -> Unit raise WriteError {
  if depth > 128 {
    raise InvalidDocument("Source rendering exceeds 128 levels")
  }
  for entry in entries {
    builder.write_string(indent)
    self.render_string(builder, entry.key, quoted)
    match entry.value {
      Text(s) => {
        builder.write_string(" ")
        self.render_string(builder, s, quoted)
        builder.write_string(self.newline())
      }
      Object(children) => {
        builder.write_string(self.newline() + indent + "{" + self.newline())
        if indent.length() > 1024 {
          raise InvalidDocument("Object indentation exceeds depth budget")
        }
        self.render_entries(
          builder,
          children,
          indent + self.indent_unit(),
          quoted,
          depth=depth + 1,
        )
        builder.write_string(indent + "}" + self.newline())
      }
    }
  }
}