///|
pub struct TemplateInspection {
  variables : Array[String]
  filters : Array[String]
  includes : Array[String]
}

///|
fn inspect_nodes(nodes : Array[Node]) -> TemplateInspection {
  let result = TemplateInspection::{ variables: [], filters: [], includes: [] }
  inspect_nodes_scoped(nodes, [], result)
  result
}

///|
fn inspect_nodes_scoped(
  nodes : Array[Node],
  locals : Array[String],
  result : TemplateInspection,
) -> Unit {
  for node in nodes {
    match node {
      Text(_) => ()
      Interpolation(expr) => inspect_expr(expr, locals, result)
      If(cond, then_nodes, else_nodes) => {
        inspect_expr(cond, locals, result)
        inspect_nodes_scoped(then_nodes, locals, result)
        inspect_nodes_scoped(else_nodes, locals, result)
      }
      For(item_name, iterable, body) => {
        inspect_expr(iterable, locals, result)
        let loop_locals = locals_with(locals, item_name)
        inspect_nodes_scoped(body, loop_locals, result)
      }
      Include(name) => add_unique(result.includes, name)
    }
  }
}

///|
fn inspect_expr(
  expr : Expr,
  locals : Array[String],
  result : TemplateInspection,
) -> Unit {
  match expr {
    Path(segments) => inspect_path(segments, locals, result)
    StringLiteral(_) => ()
    IntLiteral(_) => ()
    FloatLiteral(_) => ()
    BoolLiteral(_) => ()
    NullLiteral => ()
    FilterCall(base, name, args) => {
      inspect_expr(base, locals, result)
      add_unique(result.filters, name)
      for arg in args {
        inspect_expr(arg, locals, result)
      }
    }
    Binary(left, _, right) => {
      inspect_expr(left, locals, result)
      inspect_expr(right, locals, result)
    }
    Unary(_, inner) => inspect_expr(inner, locals, result)
  }
}

///|
fn inspect_path(
  segments : Array[String],
  locals : Array[String],
  result : TemplateInspection,
) -> Unit {
  if segments.length() == 0 {
    return
  }
  let root = segments[0]
  if root == "loop" || contains_string(locals, root) {
    return
  }
  add_unique(result.variables, segments.join("."))
}

///|
fn locals_with(locals : Array[String], name : String) -> Array[String] {
  let next : Array[String] = []
  for item in locals {
    next.push(item)
  }
  next.push(name)
  next
}

///|
fn add_unique(values : Array[String], value : String) -> Unit {
  if !contains_string(values, value) {
    values.push(value)
  }
}

///|
fn contains_string(values : Array[String], value : String) -> Bool {
  for existing in values {
    if existing == value {
      return true
    }
  }
  false
}