///|
/// Source-tooling budgets, independent of evaluator Limits. The complete tree
/// is admitted before export or formatting; parsing still has a recursive cap.
pub(all) struct SyntaxLimits {
  parser_depth : Int
  tree_depth : Int
  nodes : Int
  steps : Int
  source_bytes : Int
  output_bytes : Int
} derive(Debug)

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

///|
pub fn SyntaxLimits::default() -> SyntaxLimits {
  {
    parser_depth: 200,
    tree_depth: 4096,
    nodes: 1000000,
    steps: 1000000,
    source_bytes: 1048576,
    output_bytes: 4194304,
  }
}

///|
fn check_syntax_limits(
  limits : SyntaxLimits,
  loc : Location,
) -> Unit raise JsonnetError {
  if limits.parser_depth <= 0 ||
    limits.parser_depth > 512 ||
    limits.tree_depth <= 0 ||
    limits.tree_depth > 16384 ||
    limits.nodes <= 0 ||
    limits.steps <= 0 ||
    limits.source_bytes <= 0 ||
    limits.output_bytes <= 0 {
    raise problem(
      Limit,
      loc,
      "syntax limits must be positive; parser depth at most 512; tree depth at most 16384",
    )
  }
}

///|
fn SyntaxLimits::output_limits(self : SyntaxLimits) -> Limits {
  {
    ..Limits::default(),
    depth: self.parser_depth,
    steps: self.steps,
    source_bytes: self.source_bytes,
    output_bytes: self.output_bytes,
  }
}

///|
/// Source document admitted with independent syntax budgets. It retains exact
/// spelling and trivia and never performs evaluation or import resolution.
/// Returning its semantic AST does not grant evaluator admission.
pub struct SyntaxDocument {
  priv document : ParsedSource
}

///|
pub fn SyntaxDocument::parse(
  source : String,
  filename? : String = "",
  limits? : SyntaxLimits = SyntaxLimits::default(),
) -> SyntaxDocument raise JsonnetError {
  let loc : Location = { file: filename, line: 1, column: 1, }
  try {
    check_syntax_limits(limits, loc)
    if @utf8.encode(source).length() > limits.source_bytes {
      raise problem(Limit, loc, "source byte limit exceeded")
    }
    let tokens = lex(source, filename, retain_trivia=true)
    let expression = parse_tokens(
      tokens,
      limits.parser_depth,
      tree_depth=limits.tree_depth,
      node_limit=limits.nodes,
    )
    { document: { tokens, expression, }, }
  } catch {
    Error(d) => raise Error(d)
    e => raise problem(Runtime, loc, e.to_string())
  }
}

///|
/// Return the retained source exactly, including original line endings.
pub fn SyntaxDocument::text(self : SyntaxDocument) -> String {
  self.document.text()
}

///|
/// Return independent lexical parts with their original UTF-8 byte ranges.
pub fn SyntaxDocument::parts(self : SyntaxDocument) -> Array[SourcePart] {
  self.document.parts()
}

///|
/// Export an independent semantic AST iteratively. Program::from_ast checks
/// evaluator budgets and scope separately before execution.
pub fn SyntaxDocument::ast(self : SyntaxDocument) -> Ast {
  self.document.ast()
}

///|
/// Recheck source/tree/node budgets and format without mutating the document.
pub fn SyntaxDocument::format(
  self : SyntaxDocument,
  options? : FormatOptions = FormatOptions::default(),
  limits? : SyntaxLimits = SyntaxLimits::default(),
) -> String 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")
  }
  check_ast_depth(document.expression, limits.tree_depth) catch {
    Error(d) => raise Error(d)
    e => raise problem(Runtime, loc, e.to_string())
  }
  check_ast_nodes(document.expression, limits.nodes) catch {
    Error(d) => raise Error(d)
    e => raise problem(Runtime, loc, e.to_string())
  }
  format_tokens(
    document.tokens,
    document.expression,
    options,
    limits.output_limits(),
    loc,
    limits.tree_depth,
    Some(limits.nodes),
  )
}

///|
/// Format source under independent syntax budgets, without evaluator admission.
///
/// ```mbt check
/// test {
///   assert_eq(@moonjsonnet.format_syntax_source("[1,2]"), "[1, 2]\n")
/// }
/// ```
pub fn format_syntax_source(
  source : String,
  filename? : String = "",
  options? : FormatOptions = FormatOptions::default(),
  limits? : SyntaxLimits = SyntaxLimits::default(),
) -> String raise JsonnetError {
  SyntaxDocument::parse(source, filename~, limits~).format(options~, limits~)
}