// Top-level engine entry point.

///|
/// Render a standalone template string against a context `Value`.
pub fn render(template : String, ctx : Value) -> String {
  let (nodes, _) = parse(lex(template))
  render_nodes(nodes, ctx)
}

///|
/// A parsed template: its nodes, the `extends` parent name, and block defs.
struct Tpl {
  nodes : Array[Node]
  parent : String
  blocks : Array[(String, Array[Node])]
}

///|
/// A template engine holding multiple named templates (for inheritance/include).
pub struct Tera {
  templates : Array[(String, Tpl)]
}

///|
/// Create an empty engine.
pub fn Tera::new() -> Tera {
  Tera::{ templates: [] }
}

///|
/// Register a named template by parsing its source.
pub fn Tera::add_template(self : Tera, name : String, src : String) -> Unit {
  let (nodes, parent) = parse(lex(src))
  let blocks : Array[(String, Array[Node])] = []
  collect_blocks(nodes, blocks)
  let compiled = Tpl::{ nodes, parent, blocks }
  for index, entry in self.templates {
    let (existing_name, _) = entry
    if existing_name == name {
      self.templates[index] = (name, compiled)
      return
    }
  }
  self.templates.push((name, compiled))
}

///|
/// Return whether a template is registered under `name`.
pub fn Tera::contains_template(self : Tera, name : String) -> Bool {
  find_tpl(self.templates, name) is Some(_)
}

///|
/// Return registered template names in insertion order.
pub fn Tera::template_names(self : Tera) -> Array[String] {
  let names : Array[String] = []
  for entry in self.templates {
    let (name, _) = entry
    names.push(name)
  }
  names
}

///|
/// Return the number of registered templates.
pub fn Tera::template_count(self : Tera) -> Int {
  self.templates.length()
}

///|
/// Remove a registered template. Returns `true` when an entry was removed.
pub fn Tera::remove_template(self : Tera, name : String) -> Bool {
  for index, entry in self.templates {
    let (existing_name, _) = entry
    if existing_name == name {
      ignore(self.templates.remove(index))
      return true
    }
  }
  false
}

///|
/// Remove every registered template.
pub fn Tera::clear(self : Tera) -> Unit {
  self.templates.clear()
}

///|
/// Collect top-level `Block` definitions from a node list.
fn collect_blocks(
  nodes : Array[Node],
  acc : Array[(String, Array[Node])],
) -> Unit {
  for node in nodes {
    match node {
      Block(name, body) => acc.push((name, body))
      _ => ()
    }
  }
}

///|
/// Look up a registered template by name.
fn find_tpl(templates : Array[(String, Tpl)], name : String) -> Tpl? {
  for entry in templates {
    let (n, tpl) = entry
    if n == name {
      return Some(tpl)
    }
  }
  None
}

///|
/// Resolve the inheritance chain for `name`.
/// Returns (base_nodes, effective_blocks) where base is the top ancestor and
/// effective_blocks lists children first (so they override ancestors).
fn contains_name(names : Array[String], target : String) -> Bool {
  for name in names {
    if name == target {
      return true
    }
  }
  false
}

///|
fn with_name(names : Array[String], name : String) -> Array[String] {
  let result : Array[String] = []
  for item in names {
    result.push(item)
  }
  result.push(name)
  result
}

///|
fn validate_includes(
  nodes : Array[Node],
  templates : Array[(String, Tpl)],
  stack : Array[String],
) -> RenderError? {
  for node in nodes {
    let nested = match node {
      Include(name) => {
        if contains_name(stack, name) {
          return Some(IncludeCycle(name))
        }
        match find_tpl(templates, name) {
          None => return Some(IncludeNotFound(name))
          Some(template) =>
            validate_includes(template.nodes, templates, with_name(stack, name))
        }
      }
      If(_, then_branch, else_branch) =>
        match validate_includes(then_branch, templates, stack) {
          Some(error) => Some(error)
          None => validate_includes(else_branch, templates, stack)
        }
      For(_, _, body, empty_body) =>
        match validate_includes(body, templates, stack) {
          Some(error) => Some(error)
          None => validate_includes(empty_body, templates, stack)
        }
      Block(_, body) => validate_includes(body, templates, stack)
      _ => None
    }
    match nested {
      Some(error) => return Some(error)
      None => ()
    }
  }
  None
}

///|
fn resolve_chain(
  templates : Array[(String, Tpl)],
  name : String,
) -> Result[(Array[Node], Array[(String, Array[Node])]), RenderError] {
  let chain : Array[Tpl] = []
  let visited : Array[String] = []
  let mut current = name
  while current != "" {
    if contains_name(visited, current) {
      return Err(InheritanceCycle(current))
    }
    visited.push(current)
    match find_tpl(templates, current) {
      None =>
        if current == name {
          return Err(TemplateNotFound(current))
        } else {
          return Err(ParentNotFound(current))
        }
      Some(template) => {
        chain.push(template)
        current = template.parent
      }
    }
  }
  let base = chain[chain.length() - 1]
  let blocks : Array[(String, Array[Node])] = []
  for template in chain {
    for entry in template.blocks {
      blocks.push(entry)
    }
  }
  Ok((base.nodes, blocks))
}

///|
/// Resolve and structurally validate one named template without rendering it.
fn resolve_validated(
  templates : Array[(String, Tpl)],
  name : String,
) -> Result[(Array[Node], Array[(String, Array[Node])]), RenderError] {
  match resolve_chain(templates, name) {
    Err(error) => Err(error)
    Ok((base_nodes, blocks)) => {
      match validate_includes(base_nodes, templates, [name]) {
        Some(error) => return Err(error)
        None => ()
      }
      for entry in blocks {
        let (_, body) = entry
        match validate_includes(body, templates, [name]) {
          Some(error) => return Err(error)
          None => ()
        }
      }
      Ok((base_nodes, blocks))
    }
  }
}

///|
/// Validate every registered template without rendering output.
///
/// This preflight check catches missing parents, inheritance cycles, missing
/// includes, and include cycles anywhere in the registry, including templates
/// that have not been rendered yet.
pub fn Tera::validate(self : Tera) -> Result[Unit, RenderError] {
  for entry in self.templates {
    let (name, _) = entry
    match resolve_validated(self.templates, name) {
      Err(error) => return Err(error)
      Ok(_) => ()
    }
  }
  Ok(())
}

///|
/// Render a named template with structural validation and explicit errors.
pub fn Tera::try_render(
  self : Tera,
  name : String,
  ctx : Value,
) -> Result[String, RenderError] {
  match resolve_validated(self.templates, name) {
    Err(error) => Err(error)
    Ok((base_nodes, blocks)) => {
      let out = StringBuilder::new()
      eval_nodes(base_nodes, ctx, blocks, self, [], out)
      Ok(out.to_string())
    }
  }
}

///|
/// Render a registered template by name against a context.
///
/// This compatibility API returns an empty string on structural errors. New
/// applications should prefer `try_render` and surface its diagnostic.
pub fn Tera::render(self : Tera, name : String, ctx : Value) -> String {
  match self.try_render(name, ctx) {
    Ok(output) => output
    Err(_) => ""
  }
}