///|
/// Source-ordered visitor events. value preserves numeric spelling and comment text.
pub(all) struct Event {
  kind : String
  offset : Int
  length : Int
  path : Array[PathSegment]
  value : String
} derive(Eq, @debug.Debug)

///|
priv struct VisitFrame {
  path : Array[PathSegment]
  object : Bool
  mut key : String
  mut index : Int
  mut expecting_key : Bool
}

///| Visit a previously parsed document. Return false to stop; returns delivered count.

///|
/// Trivia whitespace is omitted; comments and separators are delivered explicitly.
pub fn visit(doc : Document, callback : (Event) -> Bool) -> Int {
  // Parse errors can truncate the tree while retaining every source token.
  // Preflight raw token nesting before allocating event paths, even if the
  // diagnostic budget was exhausted before the parser reached the depth error.
  let mut nesting = 0
  for token in doc.tokens {
    match token.kind {
      OpenBrace | OpenBracket => {
        nesting += 1
        if nesting > 512 {
          return 0
        }
      }
      CloseBrace | CloseBracket => if nesting > 0 { nesting -= 1 }
      _ => ()
    }
  }
  let mut delivered = 0
  let stack : Array[VisitFrame] = []
  for token in doc.tokens {
    let mut path : Array[PathSegment] = []
    let mut property = false
    if !stack.is_empty() {
      let parent = stack[stack.length() - 1]
      if parent.object {
        if parent.expecting_key && token.kind == StringToken {
          parent.key = token.value
          property = true
        }
        path = path_with(parent.path, Key(parent.key))
      } else {
        path = path_with(parent.path, Index(parent.index))
      }
    }
    let kind = match token.kind {
      OpenBrace | OpenBracket => {
        stack.push({
          path,
          object: token.kind == OpenBrace,
          key: "",
          index: 0,
          expecting_key: token.kind == OpenBrace,
        })
        if token.kind == OpenBrace {
          "object_begin"
        } else {
          "array_begin"
        }
      }
      CloseBrace | CloseBracket => {
        match stack.pop() {
          Some(frame) => path = frame.path
          None => ()
        }
        if token.kind == CloseBrace {
          "object_end"
        } else {
          "array_end"
        }
      }
      StringToken => if property { "property" } else { "literal" }
      NumberToken | TrueToken | FalseToken | NullToken => "literal"
      Colon | Comma => {
        if !stack.is_empty() {
          let parent = stack[stack.length() - 1]
          if token.kind == Colon {
            parent.expecting_key = false
          } else if parent.object {
            parent.expecting_key = true
            parent.key = ""
          } else {
            parent.index += 1
          }
        }
        "separator"
      }
      LineComment | BlockComment => "comment"
      Unknown => "unknown"
      _ => continue
    }
    delivered += 1
    if !callback({
        kind,
        offset: token.offset,
        length: token.length,
        path,
        value: token.value,
      }) {
      break
    }
  }
  delivered
}

///|
pub extend Event with Eq::{equal, not_equal}

///|
pub extend Event with @debug.Debug::{to_repr}