///|
/// A language a template or a `{% block lang=… %}` is read in: its name,
/// the other names it answers to, how its brace tags treat whitespace, and
/// the reader that turns its source into the tree. The three built in are
/// registered the same way a user's own is.
pub(all) struct Lang {
  name : String
  aliases : Array[String]
  options : Options
  read : (String, Reading) -> Array[Node] raise @spec.Refused
}

///|
/// Where a reader stands: the language and whether it is forced, the
/// template and the line the source starts on, the brace options in effect,
/// and the languages a block may name.
pub(all) struct Reading {
  lang : Lang
  force : Bool
  template : String
  line : Int32
  options : Options
  langs : (String) -> Lang?
}

///|
/// Whether a block in `inner` is read inside this stretch. A forced stretch
/// reads its own language and, outside HTML, HTML; any other block stays
/// text, markers and all.
pub fn Reading::allows(self : Reading, inner : Lang) -> Bool {
  !self.force ||
  inner.name == self.lang.name ||
  (self.lang.name != "html" && inner.name == "html")
}

///|
/// The reading inside a block starting at `line`: its own language and
/// options, and its own force when it gives one, else the enclosing
/// stretch's. A block in the stretch's own language keeps its options; line
/// prefixes carry through.
pub fn Reading::inside(
  self : Reading,
  lang : Lang,
  force : Bool?,
  line : Int32,
) -> Reading {
  let options = if lang.name == self.lang.name {
    self.options
  } else {
    {
      ..lang.options,
      line_statement: self.options.line_statement,
      line_comment: self.options.line_comment,
    }
  }
  { ..self, lang, force: force.unwrap_or(self.force), line, options, }
}

///|
/// A block in the language named `name`, read into the tree; a reader of a
/// user's own language calls this for the blocks it finds.
pub fn Reading::block(
  self : Reading,
  source : String,
  name : String,
  force : Bool?,
  line : Int32,
) -> Array[Node] raise @spec.Refused {
  match (self.langs)(name) {
    Some(lang) => read(source, self.inside(lang, force, line))
    None => raise Unknown(template=self.template, line~, kind="language", name~)
  }
}

///|
/// A source read in the reading's language.
pub fn read(
  source : String,
  reading : Reading,
) -> Array[Node] raise @spec.Refused {
  (reading.lang.read)(source, reading)
}

///|
/// Brace options with nothing trimmed and no line prefixes.
pub let options : Options = {
  trim_blocks: false,
  lstrip_blocks: false,
  keep_trailing_newline: false,
  line_statement: None,
  line_comment: None,
}

///|
/// HTML: text with `{{ }}`, `{% %}` and `{# #}`.
pub let html : Lang = { name: "html", aliases: [], options, read: read_braces, }

///|
/// Markdown: the brace syntax, with a tag on a line of its own leaving no
/// blank line behind.
pub let markdown : Lang = {
  name: "md",
  aliases: ["markdown"],
  options: {
    ..options,
    trim_blocks: true,
    lstrip_blocks: true,
    keep_trailing_newline: true,
  },
  read: read_braces,
}

///|
/// Pug: elements by indentation, `<` lines passed through, `{% %}` lines as
/// statements.
pub let pug : Lang = {
  name: "pug",
  aliases: ["pg"],
  options,
  read: read_indent,
}

///|
/// A language block found in the source: the name it gives, its force if
/// any, its body, the line of its head, and the offsets of the body and of
/// the end of the closing tag.
priv struct LangBlock {
  name : String
  force : Bool?
  body : String
  line : Int32
  body_at : Int32
  after : Int32
  close_sign : Char?
}

///|
/// The language a found block is read in, or `None` when the stretch is
/// forced and the block stays text. A name no language answers to is
/// refused.
fn Reading::admit(
  self : Reading,
  block : LangBlock,
) -> Lang? raise @spec.Refused {
  match (self.langs)(block.name) {
    Some(lang) => if self.allows(lang) { Some(lang) } else { None }
    None =>
      raise Unknown(
        template=self.template,
        line=block.line,
        kind="language",
        name=block.name,
      )
  }
}

///|
fn skip_blanks(src : String, at : Int32) -> Int32 {
  let mut i = at
  while i < src.length() && is_space(src.code_unit_at(i).to_int()) {
    i += 1
  }
  i
}

///|
fn word_at(src : String, at : Int32) -> (String, Int32) {
  let mut i = at
  while i < src.length() && is_name_part(src.code_unit_at(i).to_int()) {
    i += 1
  }
  (src[at:i].to_owned(), i)
}

///|
/// Where the tag whose contents end at `at` closes: past `%}`, `-%}` or
/// `+%}`, with the sign.
fn tag_close(src : String, at : Int32) -> (Int32, Char?)? {
  if starts_at(src, "-%}", at) {
    Some((at + 3, Some('-')))
  } else if starts_at(src, "+%}", at) {
    Some((at + 3, Some('+')))
  } else if starts_at(src, "%}", at) {
    Some((at + 2, None))
  } else {
    None
  }
}

///|
/// `{% block lang=… force=… %}` whose contents begin at `at`, through its
/// matching `{% endblock %}`; inheritance blocks inside are counted so their
/// end tags stay theirs. `None` when the tag is not a language block.
fn lang_block(
  src : String,
  at : Int32,
  template : String,
  first_line : Int32,
) -> LangBlock? raise @spec.Refused {
  let (word, i) = word_at(src, skip_blanks(src, at))
  if word != "block" {
    return None
  }
  let i = skip_blanks(src, i)
  if !starts_at(src, "lang=", i) {
    return None
  }
  let (name, i) = word_at(src, i + 5)
  if name.is_empty() {
    raise lang_syntax(src, i, template, first_line, "lang= names no language")
  }
  let mut i = skip_blanks(src, i)
  let mut force = None
  if starts_at(src, "force=", i) {
    let (value, j) = word_at(src, i + 6)
    force = match value {
      "true" => Some(true)
      "false" => Some(false)
      _ =>
        raise lang_syntax(
          src,
          i,
          template,
          first_line,
          "force takes true or false, not \{value}",
        )
    }
    i = skip_blanks(src, j)
  }
  guard tag_close(src, i) is Some((body_at, open_sign)) else {
    raise lang_syntax(
      src, i, template, first_line, "expected %} after the language block's head",
    )
  }
  let mut depth = 1
  let mut k = body_at
  while k < src.length() {
    if !starts_at(src, "{%", k) {
      k += 1
      continue
    }
    let mut j = k + 2
    let sign = if j < src.length() && src.code_unit_at(j) is ('-' | '+') {
      j += 1
      src.code_unit_at(j - 1).to_int().to_char()
    } else {
      None
    }
    let (inner, j) = word_at(src, skip_blanks(src, j))
    match inner {
      "block" => depth += 1
      "endblock" => {
        depth -= 1
        if depth == 0 {
          let mut end = skip_blanks(src, j)
          while end < src.length() && tag_close(src, end) is None {
            end += 1
          }
          guard tag_close(src, end) is Some((after, close_sign)) else { break }
          let mut body = src[body_at:k].to_owned()
          if open_sign == Some('-') {
            body = body.trim_start(chars=" \t\n\r\u{0B}\u{0C}").to_owned()
          }
          if sign == Some('-') {
            body = body.trim_end(chars=" \t\n\r\u{0B}\u{0C}").to_owned()
          }
          let line = line_of(src, at) + first_line - 1
          return Some({ name, force, body, line, body_at, after, close_sign, })
        }
      }
      _ => ()
    }
    k = j
  }
  raise lang_syntax(
    src,
    at,
    template,
    first_line,
    "the \{name} block is never closed with endblock",
  )
}

///|
fn line_of(src : String, at : Int32) -> Int32 {
  let mut line = 1
  for i in 0.. @spec.Refused {
  let line = line_of(src, at) + first_line - 1
  let start = match src[:at].rev_find("\n") {
    Some(n) => n + 1
    None => 0
  }
  Syntax(template~, line~, column=at - start + 1, reason~)
}