///|
/// Structural syntax categories before semantic lowering. Spelling, operators,
/// names and comments are authoritative in RawNode.parts, not this tag.
pub(all) enum RawNodeKind {
  Literal
  Number
  StringLiteral
  TextBlock
  Variable
  Parens
  Local
  Binding
  Parameter
  Argument
  If
  Function
  Array
  Object
  Field
  ObjectLocal
  ObjectAssert
  ArrayComp
  ObjectComp
  ForClause
  IfClause
  Call
  ApplyBrace
  Index
  DotIndex
  Slice
  Unary(String)
  Binary(String)
  Assert
  ErrorExpr
  Import
  ImportStr
  ImportBin
} derive(Eq, Debug)

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

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

///|
/// Ordered lossless syntax. Every token and trivia fragment occurs exactly once.
pub(all) enum RawPart {
  Text(SourcePart)
  Child(RawNode)
}

///|
/// Editable concrete syntax node. Ranges and tags describe the parsed source;
/// edits to parts change text, and reparsing refreshes metadata. Containers are
/// caller-owned; formatting reparses the edited text without evaluating it.
pub(all) struct RawNode {
  kind : RawNodeKind
  range : SourceRange?
  parts : Array[RawPart]
}

///|
/// Root syntax and the final EOF trivia, kept separately like Go's final fodder.
pub(all) struct RawAst {
  root : RawNode
  final_fodder : Array[SourcePart]
}

///|
priv struct RawSpan {
  kind : RawNodeKind
  begin : Int
  end : Int
} derive(Eq)

///|
fn Parser::record_raw(self : Parser, kind : RawNodeKind, begin : Int) -> Unit {
  if self.raw_spans is Some(spans) {
    let span : RawSpan = { kind, begin, end: self.pos, }
    // Objects are recorded at their own entry and at the primary expression.
    if spans.last() != Some(span) {
      spans.push(span)
    }
  }
}

///|
fn raw_primary_kind(expression : Expr, token : Token) -> RawNodeKind {
  if token.text == "(" {
    return Parens
  }
  if token.text == "" {
    return TextBlock
  }
  if token.text == "" {
    return StringLiteral
  }
  match expression.kind {
    Literal(_) => Literal
    NumberLiteral(_) => Number
    Var(_) | StandardFunction(_) | StandardValue(_) => Variable
    Local(_, _) => Local
    If(_, _, _) => If
    Func(_, _) => Function
    Arr(_) => Array
    Obj(_, _, _, _) => Object
    ArrayComp(_, _) => ArrayComp
    ObjectComp(_, _, _, _) => ObjectComp
    Unary(op, _) => Unary(op)
    Assert(_, _, _) => Assert
    Fail(_) => ErrorExpr
    ImportFile(_, false) => Import
    ImportFile(_, true) => ImportStr
    ImportBinary(_) => ImportBin
    Binary(op, _, _) => Binary(op)
    Call(_, _, _) => Call
    Index(_, _) => Index
    Slice(_, _, _, _) => Slice
  }
}

///|
fn raw_token_parts(
  token : Token,
  work : FormatOutput,
) -> Array[RawPart] raise JsonnetError {
  let parts = token.leading.map(t => {
    work.tick()
    Text({ kind: t.kind, text: t.raw, range: t.range, })
  })
  if !token.raw.is_empty() {
    parts.push(
      Text({
        kind: TokenText,
        text: token.raw,
        range: { begin: token.loc, end: token.end, },
      }),
    )
  }
  parts
}

///|
priv struct RawBuilt {
  begin : Int
  end : Int
  node : RawNode
  depth : Int
}

///|
fn build_raw_ast(
  tokens : Array[Token],
  spans : Array[RawSpan],
  limits : SyntaxLimits,
) -> RawAst raise JsonnetError {
  let loc = tokens[0].loc
  if spans.length() > limits.nodes {
    raise problem(Limit, loc, "raw syntax node limit exceeded")
  }
  let completed : Array[RawBuilt] = []
  let work : FormatOutput = {
    out: StringBuilder(),
    bytes: 0,
    steps: 0,
    line_start: true,
    column: 0,
    limits: limits.output_limits(),
    loc,
  }
  for span in spans {
    work.tick()
    let children : Array[RawBuilt] = []
    while completed.last() is Some(child) &&
          child.begin >= span.begin &&
          child.end <= span.end {
      work.tick()
      children.push(completed.pop().unwrap())
    }
    let parts : Array[RawPart] = []
    let mut position = span.begin
    let mut depth = 0
    for child_index = children.length() - 1
        child_index >= 0
        child_index = child_index - 1 {
      let child = children[child_index]
      depth = Int::max(depth, child.depth + 1)
      if child.begin < position {
        raise problem(Runtime, loc, "overlapping raw syntax children")
      }
      for i in position..= limits.tree_depth {
      raise problem(
        Limit,
        tokens[span.begin].loc,
        "raw syntax tree depth limit exceeded",
      )
    }
    completed.push({
      begin: span.begin,
      end: span.end,
      depth,
      node: {
        kind: span.kind,
        range: Some({
          begin: tokens[span.begin].loc,
          end: tokens[span.end - 1].end,
        }),
        parts,
      },
    })
  }
  guard completed is [root] &&
    root.begin == 0 &&
    root.end == tokens.length() - 1 else {
    raise problem(Runtime, loc, "incomplete raw syntax tree")
  }
  {
    root: root.node,
    final_fodder: tokens.last().unwrap().leading.map(t => {
      work.tick()
      { kind: t.kind, text: t.raw, range: t.range, }
    }),
  }
}

///|
/// Export a fresh concrete tree, including parentheses, fields, declarations,
/// parameters, arguments and clauses. No returned mutable container is shared.
pub fn SyntaxDocument::raw_ast(
  self : SyntaxDocument,
  limits? : SyntaxLimits = SyntaxLimits::default(),
) -> RawAst raise JsonnetError {
  let document = self.document
  let loc = document.expression.loc
  check_syntax_limits(limits, loc)
  if @utf8.encode(document.text()).length() > limits.source_bytes {
    raise problem(Limit, loc, "source byte limit exceeded")
  }
  let spans : Array[RawSpan] = []
  ignore(
    parse_tokens(
      document.tokens,
      limits.parser_depth,
      tree_depth=limits.tree_depth,
      node_limit=limits.nodes,
      raw_spans=spans,
    ),
  ) catch {
    Error(d) => raise Error(d)
    e => raise problem(Runtime, loc, e.to_string())
  }
  build_raw_ast(document.tokens, spans, limits)
}

///|
pub fn RawAst::parse(
  source : String,
  filename? : String = "",
  limits? : SyntaxLimits = SyntaxLimits::default(),
) -> RawAst raise JsonnetError {
  SyntaxDocument::parse(source, filename~, limits~).raw_ast(limits~)
}