///|
/// The template a value names: a name for the loader, or a list of names
/// of which the first that exists wins.
fn Run::resolve(
  self : Run,
  value : @spec.Value,
  missing_ok : Bool,
) -> Template? raise @spec.Refused {
  match value {
    Text(name) | Markup(name) =>
      if missing_ok {
        self.env.find(name)
      } else {
        Some(self.env.template(name))
      }
    Seq(names) | Tuple(names) => {
      for n in names {
        match self.env.find(self.call.text(n)) {
          Some(t) => return Some(t)
          None => ()
        }
      }
      if missing_ok {
        None
      } else {
        raise @spec.unknown(
          "template",
          names.map(n => n.to_string()).join(", "),
        )
      }
    }
    Undefined(name) => raise undefined_use(name)
    v =>
      raise @spec.invalid("a template name must be a string, not \{v.kind()}")
  }
}

///|
/// File every block a template defines, behind the ones already filed: a
/// child's blocks come first, then its parent's.
fn Run::file_blocks(
  self : Run,
  nodes : Array[@syntax.Node],
  template : String,
) -> Unit {
  for node in nodes {
    match node {
      BlockDef(b) => {
        match self.blocks.get(b.name) {
          Some(frames) =>
            if !frames.iter().any(f => f.template == template) {
              frames.push({ block: b, template, })
            }
          None => self.blocks[b.name] = [{ block: b, template, }]
        }
        self.file_blocks(b.body, template)
      }
      For(body~, otherwise~, ..) => {
        self.file_blocks(body, template)
        self.file_blocks(otherwise, template)
      }
      If(branches, otherwise) => {
        for b in branches {
          self.file_blocks(b.1, template)
        }
        self.file_blocks(otherwise, template)
      }
      With(_, body)
      | Autoescape(_, body)
      | FilterBlock(_, body)
      | Show(_, body)
      | Once(_, body)
      | Comment(body) => self.file_blocks(body, template)
      Element(el) => self.file_blocks(el.body, template)
      MacroDef(m) => self.file_blocks(m.body, template)
      SetBlock(_, _, body) | CallBlock(_, _, body) =>
        self.file_blocks(body, template)
      Case(_, arms, otherwise) => {
        for arm in arms {
          self.file_blocks(arm.1, template)
        }
        self.file_blocks(otherwise, template)
      }
      _ => ()
    }
  }
}

///|
/// `{% extends %}`: what follows writes nothing outside blocks, and the
/// parent renders when this template is done.
fn Run::set_parent(self : Run, value : @spec.Value) -> Unit raise @spec.Refused {
  if self.extended {
    raise @spec.invalid("extended multiple times")
  }
  match self.resolve(value, false) {
    Some(parent) => {
      self.extended = true
      self.parents.push(parent)
    }
    None => ()
  }
}

///|
/// Render a template's nodes and, if it extends another, the ancestors in
/// turn, each with its blocks filed behind the child's.
fn Run::render_chain(
  self : Run,
  nodes : Array[@syntax.Node],
) -> Unit raise @spec.Refused {
  self.file_blocks(nodes, self.template)
  self.run_nodes(nodes)
  while self.parents.pop() is Some(parent) {
    self.enter()
    self.extended = false
    self.template = parent.name
    self.file_blocks(parent.nodes, parent.name)
    self.run_nodes(parent.nodes)
    self.leave()
  }
}

///|
/// Render the block filed under `name` at `level`; `super()` renders the
/// next one down, and Pug's `append` and `prepend` place it around this one.
fn Run::render_block_at(
  self : Run,
  name : String,
  level : Int32,
  scoped : Bool,
) -> Unit raise @spec.Refused {
  let frames = self.blocks.get(name).unwrap_or([])
  if level >= frames.length() {
    return
  }
  let frame = frames[level]
  if frame.block.required &&
    level == frames.length() - 1 &&
    frames.length() == 1 {
    raise @spec.invalid("required block \{@spec.repr_text(name)} not found")
  }
  self.enter()
  defer self.leave()
  let saved_scopes = self.scopes
  let saved_template = self.template
  self.scopes = if scoped { self.scopes.copy() } else { [self.scopes[0]] }
  self.template = frame.template
  self.push(Map([]))
  defer {
    self.scopes = saved_scopes
    self.template = saved_template
  }
  if level + 1 < frames.length() {
    self.bind(
      "super",
      @spec.Value::func("super", (_, _, _) => {
        let text = self.capture(() => {
          self.render_block_at(name, level + 1, scoped)
        })
        self.rendered(text)
      }),
    )
  }
  match frame.block.placement {
    Replace => self.run_nodes(frame.block.body)
    Append => {
      self.render_block_at(name, level + 1, scoped)
      self.run_nodes(frame.block.body)
    }
    Prepend => {
      self.run_nodes(frame.block.body)
      self.render_block_at(name, level + 1, scoped)
    }
  }
}

///|
fn Run::render_block(
  self : Run,
  name : String,
  scoped~ : Bool = false,
) -> Unit raise @spec.Refused {
  self.render_block_at(name, 0, scoped)
}

///|
/// `self`: each block of the template, callable by name.
priv struct Blocks {
  run : Run
}

///|
impl @spec.Object for Blocks with fn kind(_) {
  "the template's blocks"
}

///|
impl @spec.Object for Blocks with fn attr(self, name) {
  if self.run.blocks.contains(name) {
    Some(
      @spec.Value::func(name, (_, _, _) => {
        let text = self.run.capture(() => self.run.render_block(name))
        self.run.rendered(text)
      }),
    )
  } else {
    None
  }
}

///|
/// `{% include %}`: render another template here, with the variables in
/// scope or without them; a `raw` include writes the file as it stands.
fn Run::include_template(
  self : Run,
  value : @spec.Value,
  missing~ : Bool,
  context~ : Bool,
  raw~ : Bool,
  inline~ : Bool,
  block~ : Array[@syntax.Node],
) -> Unit raise @spec.Refused {
  if raw {
    let name = self.call.text(value)
    match self.env.source(name) {
      Some(text) => self.out.write_string(text)
      None => if !missing { raise @spec.unknown("template", name) }
    }
    return
  }
  let template = match self.resolve(value, missing) {
    Some(t) => t
    None => return
  }
  self.enter()
  defer self.leave()
  let saved = (
    self.scopes,
    self.template,
    self.blocks,
    self.extended,
    self.parents,
  )
  if !inline {
    self.scopes = if context { [self.visible(), Map([])] } else { [Map([])] }
  }
  if !inline || template.nodes.iter().any(n => n is Extends(_)) {
    self.blocks = Map([])
  }
  self.template = template.name
  self.extended = false
  self.parents = []
  defer {
    self.scopes = saved.0
    self.template = saved.1
    self.blocks = saved.2
    self.extended = saved.3
    self.parents = saved.4
  }
  let before = self.autoescape()
  self.set_autoescape(template.format.autoescape)
  defer self.set_autoescape(before)
  self.render_chain(
    if block.is_empty() {
      template.nodes
    } else {
      apply_yield(template.nodes, block)
    },
  )
}

///|
/// An included template with the include's block in place of each `yield`;
/// with no `yield`, the block goes at the end of the innermost last element,
/// as Pug places it.
fn apply_yield(
  nodes : Array[@syntax.Node],
  block : Array[@syntax.Node],
) -> Array[@syntax.Node] {
  let found = Ref(false)
  fn swap(ns : Array[@syntax.Node]) -> Array[@syntax.Node] {
    let out = []
    for n in ns {
      match n {
        Yield => {
          found.val = true
          out.append(block)
        }
        Element(el) => out.push(Element({ ..el, body: swap(el.body), }))
        For(target~, iter~, cond~, recursive~, each~, body~, otherwise~) =>
          out.push(
            For(
              target~,
              iter~,
              cond~,
              recursive~,
              each~,
              body=swap(body),
              otherwise=swap(otherwise),
            ),
          )
        If(branches, otherwise) =>
          out.push(If(branches.map(b => (b.0, swap(b.1))), swap(otherwise)))
        BlockDef(b) => out.push(BlockDef({ ..b, body: swap(b.body), }))
        other => out.push(other)
      }
    }
    out
  }
  let swapped = swap(nodes)
  if found.val {
    return swapped
  }
  fn last(ns : Array[@syntax.Node]) -> Array[@syntax.Node] {
    let out = ns.copy()
    for i = out.length() - 1; i >= 0; i = i - 1 {
      match out[i] {
        Element(el) if el.body.iter().any(n => !(n is (Line(_) | Text(_)))) => {
          out[i] = Element({ ..el, body: last(el.body), })
          return out
        }
        For(target~, iter~, cond~, recursive~, each~, body~, otherwise~) if !body.is_empty() => {
          out[i] = For(
            target~,
            iter~,
            cond~,
            recursive~,
            each~,
            body=last(body),
            otherwise~,
          )
          return out
        }
        Line(_) | Text(_) => continue
        _ => break
      }
    }
    out.append(block)
    out
  }
  last(swapped)
}

///|
/// A template imported as a module: the names its top level defines.
priv struct Module {
  name : String
  fields : Map[String, @spec.Value]
}

///|
impl @spec.Object for Module with fn kind(self) {
  "the template \{self.name}"
}

///|
impl @spec.Object for Module with fn attr(self, name) {
  if name.has_prefix("_") {
    None
  } else {
    self.fields.get(name)
  }
}

///|
impl @spec.Object for Module with fn show(self) {
  "