///|
pub type Loader = (String) -> String?

///|
struct CompiledInclude {
  name : String
  source : String
  ast : Array[Node]
}

///|
pub struct Template {
  source : String
  ast : Array[Node]
  filters : Array[FilterEntry]
  loader : Loader?
  includes : Array[CompiledInclude]
  autoescape : Bool
}

///|
pub struct Engine {
  filters : Array[FilterEntry]
  loader : Loader?
  autoescape : Bool
}

///|
fn find_compiled_include(
  includes : Array[CompiledInclude],
  name : String,
) -> CompiledInclude? {
  for entry in includes {
    if entry.name == name {
      return Some(entry)
    }
  }
  None
}

///|
fn compile_named_include(
  name : String,
  loader : Loader,
  depth : Int,
  includes : Array[CompiledInclude],
) -> Unit raise MoldError {
  match find_compiled_include(includes, name) {
    Some(_) => return
    None => ()
  }
  if depth >= 10 {
    raise IncludeDepthExceeded
  }
  let source = match loader(name) {
    Some(source) => source
    None => raise MissingInclude(name)
  }
  let tokens = lex(source)
  let nodes = parse(tokens)
  includes.push({ name, source, ast: nodes })
  compile_include_nodes(nodes, loader, depth + 1, includes)
}

///|
fn compile_include_nodes(
  nodes : Array[Node],
  loader : Loader,
  depth : Int,
  includes : Array[CompiledInclude],
) -> Unit raise MoldError {
  for node in nodes {
    match node {
      Text(_) => ()
      Interpolation(_) => ()
      If(_, then_nodes, else_nodes) => {
        compile_include_nodes(then_nodes, loader, depth, includes)
        compile_include_nodes(else_nodes, loader, depth, includes)
      }
      For(_, _, body) => compile_include_nodes(body, loader, depth, includes)
      Include(name) => compile_named_include(name, loader, depth, includes)
    }
  }
}

///|
fn compile_includes(
  nodes : Array[Node],
  loader : Loader?,
) -> Array[CompiledInclude] raise MoldError {
  let includes : Array[CompiledInclude] = []
  match loader {
    None => ()
    Some(load) => compile_include_nodes(nodes, load, 0, includes)
  }
  includes
}

///|
fn parse_with_filters(
  source : String,
  filters : Array[FilterEntry],
  loader : Loader?,
  autoescape : Bool,
) -> Template raise MoldError {
  let tokens = lex(source)
  let nodes = parse(tokens)
  let includes = compile_includes(nodes, loader)
  { source, ast: nodes, filters, loader, includes, autoescape }
}

///|
pub fn Template::parse(source : String) -> Template raise MoldError {
  parse_with_filters(source, default_filter_registry(), None, false)
}

///|
pub fn Template::render(self : Template, ctx : Value) -> String raise MoldError {
  render_ast(
    self.ast,
    ctx,
    self.source,
    self.filters,
    self.loader,
    self.includes,
    self.autoescape,
  )
}

///|
pub fn Template::source(self : Template) -> String noraise {
  self.source
}

///|
pub fn Template::ast(self : Template) -> Array[Node] noraise {
  self.ast
}

///|
pub fn Template::inspect(self : Template) -> TemplateInspection noraise {
  inspect_nodes(self.ast)
}

///|
pub fn Engine::new() -> Engine noraise {
  Engine::{
    filters: default_filter_registry(),
    loader: None,
    autoescape: false,
  }
}

///|
pub fn Engine::with_loader(self : Engine, loader : Loader) -> Engine noraise {
  Engine::{
    filters: self.filters,
    loader: Some(loader),
    autoescape: self.autoescape,
  }
}

///|
pub fn Engine::with_autoescape(
  self : Engine,
  autoescape : Bool,
) -> Engine noraise {
  Engine::{ filters: self.filters, loader: self.loader, autoescape }
}

///|
pub fn Engine::register_filter(
  self : Engine,
  name : String,
  filter : Filter,
) -> Unit raise MoldError {
  register_filter_entry(self.filters, name, filter)
}

///|
pub fn Engine::parse(
  self : Engine,
  source : String,
) -> Template raise MoldError {
  parse_with_filters(
    source,
    copy_filter_registry(self.filters),
    self.loader,
    self.autoescape,
  )
}

///|
pub fn Engine::render(
  self : Engine,
  source : String,
  ctx : Value,
) -> String raise MoldError {
  self.parse(source).render(ctx)
}

///|
pub fn Engine::inspect(
  self : Engine,
  source : String,
) -> TemplateInspection raise MoldError {
  self.parse(source).inspect()
}

///|
pub fn render(source : String, ctx : Value) -> String raise MoldError {
  Engine::new().render(source, ctx)
}

///|
pub fn inspect(source : String) -> TemplateInspection raise MoldError {
  Template::parse(source).inspect()
}