///|
/// Canonical rendering and structural metrics for compiled expressions.
///
/// The renderer reproduces an expression as a canonical string: minimal
/// parentheses derived from operator precedence, single spaces around binary
/// operators, and JSON-quoted literals. Re-compiling a rendered expression
/// yields an equivalent tree, which enables round-trip guarantees.

///|
fn Expression::precedence_of(self : Expression) -> Int32 {
  match self {
    Literal(_) | Variable(_) | Call(_, _) => 100
    Unary(_, _) => 7
    Binary(op, _, _) => precedence(op)
  }
}

///|
fn render_scalar(value : Value) -> String {
  match value {
    Null => "null"
    Bool(true) => "true"
    Bool(false) => "false"
    Number(n) => Json::number(n).stringify()
    String(s) => Json::string(s).stringify()
    other => other.stringify()
  }
}

///|
fn Expression::render_prec(self : Expression, minimum : Int32) -> String {
  let text = self.render()
  if self.precedence_of() < minimum {
    "(" + text + ")"
  } else {
    text
  }
}

///|
fn Expression::render(self : Expression) -> String {
  match self {
    Literal(value) => render_scalar(value)
    Variable(path) => path
    Unary(op, operand) =>
      if op == "-" {
        "-" + operand.render_prec(7)
      } else {
        op + operand.render_prec(8)
      }
    Binary(op, left, right) => {
      let level = precedence(op)
      left.render_prec(level) + " " + op + " " + right.render_prec(level + 1)
    }
    Call(name, args) => {
      let parts : Array[String] = []
      for arg in args {
        parts.push(arg.render())
      }
      name + "(" + parts.join(", ") + ")"
    }
  }
}

///|
/// Structural metrics: total node count and maximum nesting depth.
fn Expression::metrics(self : Expression) -> (Int32, Int32) {
  match self {
    Literal(_) | Variable(_) => (1, 1)
    Unary(_, operand) => {
      let (nodes, depth) = operand.metrics()
      (nodes + 1, depth + 1)
    }
    Binary(_, left, right) => {
      let (ln, ld) = left.metrics()
      let (rn, rd) = right.metrics()
      let deeper = if ld > rd { ld } else { rd }
      (ln + rn + 1, deeper + 1)
    }
    Call(_, args) => {
      let mut nodes = 1
      let mut depth = 0
      for arg in args {
        let (n, d) = arg.metrics()
        nodes = nodes + n
        if d > depth {
          depth = d
        }
      }
      (nodes, depth + 1)
    }
  }
}

///|
/// Full leaf-to-root description of an expression tree for diagnostics.
fn Expression::describe(self : Expression, lines : Array[String]) -> Unit {
  let prefix = lines.length().to_string()
  match self {
    Literal(value) => lines.push(prefix + " literal " + render_scalar(value))
    Variable(path) => lines.push(prefix + " variable " + path)
    Unary(op, operand) => {
      lines.push(prefix + " unary " + op)
      operand.describe(lines)
    }
    Binary(op, left, right) => {
      lines.push(prefix + " binary " + op)
      left.describe(lines)
      right.describe(lines)
    }
    Call(name, args) => {
      lines.push(prefix + " call " + name + "/" + args.length().to_string())
      for arg in args {
        arg.describe(lines)
      }
    }
  }
}

///|
fn serialize_request(request : Value) -> Value raise {
  let expr = compile(str(get(request, "expression")))
  let (nodes, depth) = expr.metrics()
  let lines : Array[String] = []
  expr.describe(lines)
  let tree : Array[Value] = lines.map(fn(line) { String(line) })
  record([
    ("canonical", String(expr.render())),
    ("nodes", Number(nodes.to_double())),
    ("depth", Number(depth.to_double())),
    ("tree", Array(tree)),
  ])
}

///|
/// Re-compiles a canonical rendering and verifies behavioral equivalence
/// against the original expression over the supplied sample facts.
fn roundtrip_request(request : Value) -> Value raise {
  let source = str(get(request, "expression"))
  let facts = get(request, "facts")
  let budget = optional_num(get(request, "budget"), 10000.0).to_int()
  if budget <= 0 || budget > 1000000 {
    raise input_error("E3007", "budget out of range")
  }
  let original = compile(source)
  let rendered = original.render()
  let reparsed = compile(rendered)
  let state_a : Evaluation = { fuel: budget, trace: [], }
  let state_b : Evaluation = { fuel: budget, trace: [], }
  let first = evaluate(original, facts, state_a)
  let second = evaluate(reparsed, facts, state_b)
  let same = first == second
  let (nodes, depth) = original.metrics()
  record([
    ("equivalent", Bool(same)),
    ("value", first),
    ("canonical", String(rendered)),
    ("nodes", Number(nodes.to_double())),
    ("depth", Number(depth.to_double())),
  ])
}