// https://github.com/alexkappa/mustache/blob/master/mustache.go

///|
priv enum Node {
  DelimNode
  TextNode(String)
  VarNode(VarNode)
  CommentNode(String)
  SectionNode(SectionNode)
  PartialNode(String)
} derive(Show, Eq)

///|
fn Node::render(
  self : Node,
  t : RenderableTemplate,
  context : Context,
) -> String raise RenderingError {
  match self {
    SectionNode(section) => section.render(context, t)
    VarNode(v) =>
      lookup(v.name, context)
      .map(value => Node::print_json(value))
      .map(printed => if v.escape { Node::escape(printed) } else { printed })
      .unwrap_or_error(RenderingError("failed to lookup"))
    TextNode(text) => text
    DelimNode => ""
    CommentNode(_) => ""
    PartialNode(part) => {
      let template = t.partials.get(part)
      match template {
        Some(template) => template.with_partials(t.partials).render(context)
        None => ""
      }
    }
  }
}

///|
fn Node::print_json(json : Json) -> String {
  match json {
    String(s) => s
    Number(n, ..) => n.to_string()
    False => "false"
    True => "true"
    Null => "null"
    _ => json.to_string()
  }
}

///|
fn Node::escape(str : String) -> String {
  if !(str.contains_char('\'') ||
    str.contains_char('"') ||
    str.contains_char('&') ||
    str.contains_char('<') ||
    str.contains_char('>')) {
    return str
  }
  str
  .replace_all(old="'", new=""")
  .replace_all(old="\"", new="'")
  .replace_all(old="&", new="&")
  .replace_all(old="<", new="<")
  .replace_all(old=">", new=">")
}

///|
priv struct VarNode {
  name : String
  escape : Bool
} derive(Show, Eq)

///|
priv struct SectionNode {
  name : String
  inverted : Bool
  elems : Array[Node]
} derive(Show, Eq)

///|
fn SectionNode::render(
  self : SectionNode,
  ctx : Context,
  t : RenderableTemplate,
) -> String raise RenderingError {
  fn renderElems(appendCtx : Context) -> String raise RenderingError {
    self.elems.fold(init="", (result, node) => result +
      node.render(t, Context([..appendCtx.0, ..ctx.0])))
  }

  let value = lookup(self.name, ctx)
  let is_falsy = is_falsy(value.unwrap_or(Json::null()))
  if !is_falsy != self.inverted {
    match value {
      Some(Array(array)) =>
        array
        .map(json => Context([json]))
        .fold(init="", (result, ctx) => result + renderElems(ctx))
      _ =>
        renderElems(
          if value is None {
            Context([])
          } else {
            Context([value.unwrap()])
          },
        )
    }
  } else {
    return ""
  }
}

///|
test {
  let t = Template::new("")
  let renderable = t.parse_template("hello {{^world}}world{{/world}}")
  let rendered = renderable.render([
    Json::object({ "world": Json::boolean(false) }),
  ])
  assert_eq(rendered, "hello world")
  let rendered = renderable.render([
    Json::object({ "world": Json::boolean(true) }),
  ])
  assert_eq(rendered, "hello ")
}

///|
test {
  let t = Template::new("")
  let renderable = t.parse_template("hello {{#world}}world{{/world}}")
  let rendered = renderable.render([
    Json::object({ "world": Json::boolean(true) }),
  ])
  assert_eq(rendered, "hello world")
  let rendered = renderable.render([
    Json::object({ "world": Json::boolean(false) }),
  ])
  assert_eq(rendered, "hello ")
}

///|
priv struct Context(Array[Json])

///|
priv suberror RenderingError String derive(Show)

// The Template type represents a template and its components.

///|
priv struct Template {
  name : String
  partials : Map[String, RenderableTemplate]
  _start_delim : String
  _end_delim : String
  silentMiss : Bool
}

///|
fn Template::parse_template(
  self : Template,
  input : String,
) -> RenderableTemplate raise ParseError {
  let l = Lexer::new(self.name, input)
  let p = Parser::new(l)
  { partials: self.partials, elms: p.parse(), silentMiss: self.silentMiss }
}

// TODO: fixme

///|
fn Template::new(name : String) -> Template {
  { name, partials: {}, _start_delim: "", _end_delim: "", silentMiss: false }
}

///|
priv struct RenderableTemplate {
  partials : Map[String, RenderableTemplate]
  elms : Array[Node]
  silentMiss : Bool
}

///|
fn RenderableTemplate::with_partials(
  self : RenderableTemplate,
  partials : Map[String, RenderableTemplate],
) -> RenderableTemplate {
  { partials, elms: self.elms, silentMiss: self.silentMiss }
}

///|
fn RenderableTemplate::render(
  self : RenderableTemplate,
  context : Context,
) -> String raise RenderingError {
  let mut result = ""
  for elem in self.elms {
    let renderedElem = try? elem.render(self, context)
    match renderedElem {
      Err(e) => if !self.silentMiss { raise e }
      Ok(rendered) => result = result + rendered
    }
  }
  result
}